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: 不要なデータも含めて返される
// GET /users/123
{
"id": "123",
"name": "田中太郎",
"email": "tanaka@example.com",
"phone": "090-1234-5678", // 不要
"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 });
キャッシュ戦略
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"
}
}
]
}
パフォーマンス特性
比較表
| 観点 | REST | GraphQL |
|---|---|---|
| リクエスト数 | 多くなりがち | 最小化可能 |
| ペイロードサイズ | オーバーフェッチ | 最適化 |
| 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: エンドポイントごとの制御
// 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特有のセキュリティ対策
// 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が適しているケース
REST を選ぶべき状況:
✓ シンプルなCRUD操作が中心
✓ HTTPキャッシュを最大限活用したい
✓ CDN/エッジキャッシュが重要
✓ チームがREST経験豊富
✓ 公開APIとして提供
✓ ファイルアップロード/ダウンロードが多い
GraphQLが適しているケース
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 の強み
- 柔軟なデータ取得
- 型安全なスキーマ
- 単一リクエストで複雑なデータ取得
- 開発者体験の向上
選択の指針
- 要件を分析: データの複雑さ、クライアントの多様性
- チームのスキル: 既存の経験と学習コスト
- パフォーマンス要件: キャッシュ、帯域幅、レイテンシ
- 将来の拡張性: スケーラビリティ、保守性
どちらを選んでも、適切に設計・実装すれば優れたAPIを構築できます。重要なのは、プロジェクトの特性に合った選択をすることです。