GraphQL vs REST API - 設計思想と選択基準

16分 で読める | 2025.12.02

公式ドキュメント

API設計において「GraphQLとREST、どちらを選ぶべきか」は頻繁に議論されるテーマです。両者は根本的に異なる設計思想を持っており、どちらが優れているかではなく、プロジェクトの要件に応じた適切な選択が重要です。本記事では、両者の違いを深く理解し、正しい判断ができるようになることを目指します。

設計思想の違い

REST: リソース指向アーキテクチャ

RESTは「リソース」を中心とした設計思想です。

flowchart TB
 subgraph REST["REST の設計原則"]
 URI["/users/123<br/>User リソースを一意に識別するURI"]

 subgraph Methods["HTTPメソッドがアクションを表現"]
 GET["GET /users/123 → ユーザー取得"]
 PUT["PUT /users/123 → ユーザー更新"]
 DELETE["DELETE /users/123 → ユーザー削除"]
 POST["POST /users → ユーザー作成"]
 end
 end

GraphQL: クエリ言語アプローチ

GraphQLは「クライアントが必要なデータを宣言的に取得する」設計思想です。

flowchart TB
 subgraph GraphQL["GraphQL の設計原則"]
 Endpoint["/graphql<br/>単一エンドポイント"]

 subgraph Query["クエリで必要なデータを指定"]
 Q1["user(id: '123')"]
 Q2["→ name"]
 Q3["→ email"]
 Q4["→ posts(limit: 5)"]
 Q5["  → title"]
 end

 Endpoint --> Query
 end

データ取得パターンの比較

ポイント: RESTはHTTPメソッド(GET/POST/PUT/DELETE)でアクションを表現し、GraphQLは単一エンドポイントでクエリによってデータ取得を行います。この違いがキャッシュ戦略やエラーハンドリングに大きく影響します

オーバーフェッチ問題

// REST: 不要なデータも含めて返される
// GET /users/123
{
 "id": "123",
 "name": "田中太郎",
 "email": "tanaka@example.com",
 "phone": "000-0000-0000", // 不要
 "address": "東京都...", // 不要
 "createdAt": "2024-01-01", // 不要
 "updatedAt": "2024-12-01", // 不要
 "settings": { ... }, // 不要
 "profile": { ... } // 不要
}

// GraphQL: 必要なフィールドのみ取得
// query { user(id: "123") { name, email } }
{
 "data": {
 "user": {
 "name": "田中太郎",
 "email": "tanaka@example.com"
 }
 }
}

アンダーフェッチ問題(N+1リクエスト)

// REST: 複数のリクエストが必要
// 1. GET /users/123
// 2. GET /users/123/posts
// 3. GET /posts/1/comments
// 4. GET /posts/2/comments
// ... (N+1問題)

// GraphQL: 1リクエストで全データ取得
query {
 user(id: "123") {
 name
 posts {
 title
 comments {
 content
 author { name }
 }
 }
 }
}

スキーマ定義

REST: OpenAPI/Swagger

# openapi.yaml
openapi: 3.0.0
info:
 title: User API
 version: 1.0.0

paths:
 /users/{id}:
 get:
 summary: ユーザー取得
 parameters:
 - name: id
 in: path
 required: true
 schema:
 type: string
 responses:
 '200':
 description: 成功
 content:
 application/json:
 schema:
 $ref: '#/components/schemas/User'

components:
 schemas:
 User:
 type: object
 properties:
 id:
 type: string
 name:
 type: string
 email:
 type: string
 format: email

GraphQL: SDL(Schema Definition Language)

# schema.graphql
type User {
 id: ID!
 name: String!
 email: String!
 posts(limit: Int, offset: Int): [Post!]!
 createdAt: DateTime!
}

type Post {
 id: ID!
 title: String!
 content: String!
 author: User!
 comments: [Comment!]!
}

type Comment {
 id: ID!
 content: String!
 author: User!
}

type Query {
 user(id: ID!): User
 users(filter: UserFilter): [User!]!
 post(id: ID!): Post
}

type Mutation {
 createUser(input: CreateUserInput!): User!
 updateUser(id: ID!, input: UpdateUserInput!): User!
 deleteUser(id: ID!): Boolean!
}

input CreateUserInput {
 name: String!
 email: String!
}

実装比較

RESTサーバー(Express)

// REST API (Express + TypeScript)
import express from 'express';

const app = express();
app.use(express.json());

// ユーザー取得
app.get('/users/:id', async (req, res) => {
 const user = await db.user.findUnique({
 where: { id: req.params.id }
 });

 if (!user) {
 return res.status(404).json({ error: 'User not found' });
 }

 res.json(user);
});

// ユーザーの投稿取得
app.get('/users/:id/posts', async (req, res) => {
 const posts = await db.post.findMany({
 where: { authorId: req.params.id },
 take: parseInt(req.query.limit as string) || 10,
 skip: parseInt(req.query.offset as string) || 0
 });

 res.json(posts);
});

// ユーザー作成
app.post('/users', async (req, res) => {
 const { name, email } = req.body;

 const user = await db.user.create({
 data: { name, email }
 });

 res.status(201).json(user);
});

GraphQLサーバー(Apollo Server)

// GraphQL API (Apollo Server + TypeScript)
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

const typeDefs = `#graphql
 type User {
 id: ID!
 name: String!
 email: String!
 posts(limit: Int, offset: Int): [Post!]!
 }

 type Post {
 id: ID!
 title: String!
 content: String!
 author: User!
 }

 type Query {
 user(id: ID!): User
 users: [User!]!
 }

 type Mutation {
 createUser(name: String!, email: String!): User!
 }
`;

const resolvers = {
 Query: {
 user: async (_, { id }) => {
 return db.user.findUnique({ where: { id } });
 },
 users: async () => {
 return db.user.findMany();
 }
 },

 Mutation: {
 createUser: async (_, { name, email }) => {
 return db.user.create({ data: { name, email } });
 }
 },

 // フィールドレベルのリゾルバー
 User: {
 posts: async (parent, { limit = 10, offset = 0 }) => {
 return db.post.findMany({
 where: { authorId: parent.id },
 take: limit,
 skip: offset
 });
 }
 }
};

const server = new ApolloServer({ typeDefs, resolvers });

キャッシュ戦略

注意: GraphQLでは1リクエストで大量のデータを取得できるため、サーバー側でN+1問題が発生しやすくなります。DataLoaderなどのバッチ処理ライブラリの導入を検討してください。

REST: HTTPキャッシュの活用

// REST: 標準的なHTTPキャッシュ
app.get('/users/:id', async (req, res) => {
 const user = await getUser(req.params.id);

 res
 .set('Cache-Control', 'public, max-age=300') // 5分キャッシュ
 .set('ETag', `"${user.version}"`)
 .json(user);
});

// クライアント側
fetch('/users/123', {
 headers: {
 'If-None-Match': '"v1"' // 条件付きリクエスト
 }
});
// → 304 Not Modified(キャッシュ有効)

GraphQL: クライアントサイドキャッシュ

// GraphQL: Apollo Client の正規化キャッシュ
import { ApolloClient, InMemoryCache } from '@apollo/client';

const client = new ApolloClient({
 uri: '/graphql',
 cache: new InMemoryCache({
 typePolicies: {
 User: {
 keyFields: ['id'], // キャッシュのキー
 },
 Post: {
 keyFields: ['id'],
 }
 }
 })
});

// 自動的にキャッシュが正規化される
// User:123 → { name: "...", email: "..." }
// Post:456 → { title: "...", author: { __ref: "User:123" } }

エラーハンドリング

REST: HTTPステータスコード

// REST: 標準的なHTTPステータスコード
app.get('/users/:id', async (req, res) => {
 try {
 const user = await getUser(req.params.id);

 if (!user) {
 return res.status(404).json({
 error: 'NOT_FOUND',
 message: 'ユーザーが見つかりません'
 });
 }

 res.json(user);
 } catch (error) {
 res.status(500).json({
 error: 'INTERNAL_ERROR',
 message: 'サーバーエラーが発生しました'
 });
 }
});

// レスポンス例
// HTTP 404
{
 "error": "NOT_FOUND",
 "message": "ユーザーが見つかりません"
}

GraphQL: errors配列

// GraphQL: 部分的なエラーが可能
const resolvers = {
 Query: {
 user: async (_, { id }) => {
 const user = await getUser(id);
 if (!user) {
 throw new GraphQLError('ユーザーが見つかりません', {
 extensions: {
 code: 'NOT_FOUND',
 argumentName: 'id'
 }
 });
 }
 return user;
 }
 }
};

// レスポンス例(部分的成功)
{
 "data": {
 "user": null,
 "posts": [...] // 他のフィールドは成功
 },
 "errors": [
 {
 "message": "ユーザーが見つかりません",
 "path": ["user"],
 "extensions": {
 "code": "NOT_FOUND"
 }
 }
 ]
}

パフォーマンス特性

比較表

観点RESTGraphQL
リクエスト数多くなりがち最小化可能
ペイロードサイズオーバーフェッチ最適化
HTTPキャッシュネイティブ対応追加実装が必要
CDNキャッシュ容易工夫が必要
N+1問題(サーバー)なしDataLoaderで対策

GraphQLのN+1問題対策

// DataLoader による最適化
import DataLoader from 'dataloader';

// バッチローダーの定義
const userLoader = new DataLoader(async (userIds: string[]) => {
 const users = await db.user.findMany({
 where: { id: { in: userIds } }
 });

 // IDの順序を保持して返す
 const userMap = new Map(users.map(u => [u.id, u]));
 return userIds.map(id => userMap.get(id));
});

// リゾルバーで使用
const resolvers = {
 Post: {
 author: (parent, _, context) => {
 return context.userLoader.load(parent.authorId);
 }
 }
};

// 結果: 個別のクエリではなくバッチクエリに
// SELECT * FROM users WHERE id IN ('1', '2', '3', ...)

セキュリティ考慮事項

実践メモ: RESTではHTTP標準のキャッシュ機構(ETag、Cache-Control)をそのまま活用できますが、GraphQLではApollo Clientの正規化キャッシュなど、クライアントサイドでのキャッシュ実装が必要です。

REST: エンドポイントごとの制御

// REST: ルートごとに認可
app.get('/admin/users', requireAdmin, async (req, res) => {
 // 管理者のみアクセス可能
});

app.get('/users/:id', async (req, res) => {
 // 公開エンドポイント
});

GraphQL: フィールドレベルの制御

// GraphQL: ディレクティブによる制御
const typeDefs = `#graphql
 directive @auth(requires: Role!) on FIELD_DEFINITION

 type User {
 id: ID!
 name: String!
 email: String! @auth(requires: OWNER)
 salary: Int @auth(requires: ADMIN)
 }
`;

// クエリ複雑性の制限
import { createComplexityLimitRule } from 'graphql-validation-complexity';

const server = new ApolloServer({
 typeDefs,
 resolvers,
 validationRules: [
 createComplexityLimitRule(1000) // 複雑性の上限
 ]
});

GraphQLではフィールドレベルの認可が必要であり、RESTのようなエンドポイント単位の制御よりも細かい設計が求められます。

GraphQL特有のセキュリティ対策

// 1. クエリ深度の制限
import depthLimit from 'graphql-depth-limit';

const server = new ApolloServer({
 validationRules: [depthLimit(10)] // 最大深度10
});

// 2. インタロスペクションの無効化(本番環境)
const server = new ApolloServer({
 introspection: process.env.NODE_ENV !== 'production'
});

// 3. 永続クエリ
const server = new ApolloServer({
 persistedQueries: {
 cache: new RedisCache()
 }
});

選択基準

RESTが適しているケース

  • ✓ シンプルなCRUD操作が中心
  • ✓ HTTPキャッシュを最大限活用したい
  • ✓ CDN/エッジキャッシュが重要
  • ✓ チームがREST経験豊富
  • ✓ 公開APIとして提供
  • ✓ ファイルアップロード/ダウンロードが多い

GraphQLが適しているケース

  • ✓ 複雑なデータ要件(多くのリレーション)
  • ✓ 複数のクライアント(Web、Mobile、etc.)
  • ✓ クライアント主導の開発(フロントエンド優先)
  • ✓ リアルタイム機能(Subscriptions)
  • ✓ マイクロサービスのBFF(Backend for Frontend)
  • ✓ 帯域幅の最適化が重要(モバイル)

ハイブリッドアプローチ

// 両方を併用するパターン
const app = express();

// REST: ファイルアップロード
app.post('/upload', upload.single('file'), (req, res) => {
 res.json({ url: req.file.path });
});

// REST: ヘルスチェック
app.get('/health', (req, res) => {
 res.json({ status: 'ok' });
});

// GraphQL: データ取得/更新
app.use('/graphql', apolloMiddleware);

まとめ

GraphQLとRESTは、それぞれ異なる強みを持つAPI設計パラダイムです。

REST の強み

  • HTTPの機能を最大限活用
  • シンプルで理解しやすい
  • 豊富なツールエコシステム
  • キャッシュが容易

GraphQL の強み

  • 柔軟なデータ取得
  • 型安全なスキーマ
  • 単一リクエストで複雑なデータ取得
  • 開発者体験の向上

選択の指針

  1. 要件を分析: データの複雑さ、クライアントの多様性
  2. チームのスキル: 既存の経験と学習コスト
  3. パフォーマンス要件: キャッシュ、帯域幅、レイテンシ
  4. 将来の拡張性: スケーラビリティ、保守性

どちらを選んでも、適切に設計・実装すれば優れたAPIを構築できます。重要なのは、プロジェクトの特性に合った選択をすることです。

参考リンク

参考リソース

← 一覧に戻る
PR
PR
PR
PR