メインコンテンツにスキップ

🔄 REST API vs GraphQL

📖 定義

REST APIは、HTTPメソッドを使用してリソース中心に設計された従来のAPIアーキテクチャです。GraphQLは、Facebookが開発したクエリ言語で、クライアントが必要なデータを正確にリクエストできる柔軟なAPI方式です。RESTは複数のエンドポイントを持ち、GraphQLは単一のエンドポイントですべてのデータを処理します。

🎯 比喩で理解する

レストラン vs ビュッフェ

REST API = レストランメニュー
├─ 決められたメニューのみ注文可能
├─ 各メニューごとに決められた構成
├─ 「ピザください」→ ピザ全体が来る
├─ トッピングだけ抜いたり追加したりが難しい
└─ シンプルで予測可能

GraphQL = ビュッフェ
├─ 好きな料理だけ選んで取ってくる
├─ 必要な分だけ選択
├─ 「チーズだけください」→ チーズだけもらえる
├─ 自由に組み合わせ可能
└─ 柔軟だが複雑になることも

図書館の本の貸出

REST API = 従来の貸出
司書:「どんな本が必要ですか?」
私:「コンピューター関連の本をください」
司書:「これら全部お渡しします」(本10冊)
私:「1章だけ必要なんですが...」(残りは不要)

GraphQL = スマート貸出
私:「コンピューター関連の本の3章だけ必要です」
司書:「3章だけコピーしてお渡しします」(正確に必要なものだけ)
私:「完璧です!」

⚙️ 動作原理

1. データリクエスト方式の比較

REST API: 複数のエンドポイント
GET /users/1 → ユーザー情報
GET /users/1/posts → ユーザーの投稿
GET /posts/1/comments → 投稿のコメント

合計3回のリクエストが必要!

GraphQL: 単一のエンドポイント
POST /graphql
{
user(id: 1) {
name
posts {
title
comments {
text
}
}
}
}

1回のリクエストですべてのデータ!

2. Over-fetching vs Under-fetching

REST APIの問題

Over-fetching(不要なデータを受信)
GET /users/1
{
"id": 1,
"name": "田中太郎",
"email": "tanaka@example.com",
"phone": "090-1234-5678",
"address": "東京都...",
"createdAt": "2024-01-01",
// 名前だけ必要なのにすべての情報を受信!
}

Under-fetching(不足したデータ)
GET /users/1 → ユーザー情報
GET /users/1/posts → 追加リクエスト必要
GET /users/1/friends → さらに追加リクエスト
// 何度もリクエストが必要!

GraphQLの解決

正確に必要なものだけ
{
user(id: 1) {
name // 名前だけリクエスト!
}
}
→ { "name": "田中太郎" }

一度にすべて
{
user(id: 1) {
name
posts { title }
friends { name }
}
}
→ すべてのデータを1回で!

3. API設計哲学

REST: リソース中心
┌─────────────────┐
│ /users │ → ユーザー一覧
│ /users/1 │ → 特定のユーザー
│ /posts │ → 投稿一覧
│ /posts/1 │ → 特定の投稿
└─────────────────┘
各リソースごとにエンドポイント

GraphQL: クエリ中心
┌─────────────────┐
│ /graphql │ → すべてのリクエスト
└─────────────────┘

┌────────┐
│ Query │ → データ読み取り
│Mutation│ → データ変更
│Subscribe│ → リアルタイム購読
└────────┘

💡 実際の例

REST API例 (Express.js)

// Express.js REST API実装
const express = require('express');
const app = express();

app.use(express.json());

// データ(実際にはデータベースを使用)
const users = [
{
id: 1,
name: '田中太郎',
email: 'tanaka@example.com',
age: 25
},
{
id: 2,
name: '佐藤花子',
email: 'sato@example.com',
age: 30
}
];

const posts = [
{
id: 1,
userId: 1,
title: 'REST API入門',
content: 'RESTは...'
},
{
id: 2,
userId: 1,
title: 'GraphQL入門',
content: 'GraphQLは...'
}
];

// ========== GET: 全ユーザー取得 ==========
app.get('/api/users', (req, res) => {
res.json(users);
});

// ========== GET: 特定ユーザー取得 ==========
app.get('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: 'ユーザーが見つかりません' });
}
res.json(user);
});

// ========== POST: ユーザー作成 ==========
app.post('/api/users', (req, res) => {
const newUser = {
id: users.length + 1,
name: req.body.name,
email: req.body.email,
age: req.body.age
};
users.push(newUser);
res.status(201).json(newUser);
});

// ========== PUT: ユーザー更新 ==========
app.put('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: 'ユーザーが見つかりません' });
}

user.name = req.body.name || user.name;
user.email = req.body.email || user.email;
user.age = req.body.age || user.age;

res.json(user);
});

// ========== DELETE: ユーザー削除 ==========
app.delete('/api/users/:id', (req, res) => {
const index = users.findIndex(u => u.id === parseInt(req.params.id));
if (index === -1) {
return res.status(404).json({ error: 'ユーザーが見つかりません' });
}

users.splice(index, 1);
res.status(204).send();
});

// ========== GET: ユーザーの投稿取得 ==========
app.get('/api/users/:id/posts', (req, res) => {
const userId = parseInt(req.params.id);
const userPosts = posts.filter(p => p.userId === userId);
res.json(userPosts);
});

// ========== GET: 特定の投稿取得 ==========
app.get('/api/posts/:id', (req, res) => {
const post = posts.find(p => p.id === parseInt(req.params.id));
if (!post) {
return res.status(404).json({ error: '投稿が見つかりません' });
}
res.json(post);
});

app.listen(3000, () => {
console.log('REST APIサーバー実行中: http://localhost:3000');
});

REST APIクライアント使用

// ========== REST API使用 ==========

// 1. ユーザー一覧取得
const response1 = await fetch('http://localhost:3000/api/users');
const users = await response1.json();
console.log(users);

// 2. 特定ユーザー取得
const response2 = await fetch('http://localhost:3000/api/users/1');
const user = await response2.json();
console.log(user);

// 3. ユーザー作成
const response3 = await fetch('http://localhost:3000/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: '鈴木一郎',
email: 'suzuki@example.com',
age: 28
})
});
const newUser = await response3.json();

// 4. ユーザー更新
const response4 = await fetch('http://localhost:3000/api/users/1', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
age: 26
})
});

// 5. ユーザー削除
await fetch('http://localhost:3000/api/users/1', {
method: 'DELETE'
});

// ========== 問題: 複数のリクエストが必要 ==========
// ユーザーと投稿を一緒に取得するには?
const userResponse = await fetch('http://localhost:3000/api/users/1');
const user = await userResponse.json();

const postsResponse = await fetch('http://localhost:3000/api/users/1/posts');
const posts = await postsResponse.json();

console.log({ user, posts }); // 2回のリクエスト!

GraphQL例 (Apollo Server)

// GraphQLサーバー実装
const { ApolloServer, gql } = require('apollo-server');

// データ(実際にはデータベースを使用)
const users = [
{ id: 1, name: '田中太郎', email: 'tanaka@example.com', age: 25 },
{ id: 2, name: '佐藤花子', email: 'sato@example.com', age: 30 }
];

const posts = [
{ id: 1, userId: 1, title: 'REST API入門', content: 'RESTは...' },
{ id: 2, userId: 1, title: 'GraphQL入門', content: 'GraphQLは...' },
{ id: 3, userId: 2, title: 'Node.jsチュートリアル', content: 'Nodeは...' }
];

// ========== スキーマ定義 (Type Definitions) ==========
const typeDefs = gql`
# ユーザータイプ
type User {
id: Int!
name: String!
email: String!
age: Int
posts: [Post!]! # ユーザーの投稿(リレーション)
}

# 投稿タイプ
type Post {
id: Int!
title: String!
content: String!
author: User! # 投稿作成者(リレーション)
}

# クエリ(データ読み取り)
type Query {
# 全ユーザー
users: [User!]!

# 特定ユーザー
user(id: Int!): User

# 全投稿
posts: [Post!]!

# 特定投稿
post(id: Int!): Post
}

# ミューテーション(データ変更)
type Mutation {
# ユーザー作成
createUser(name: String!, email: String!, age: Int): User!

# ユーザー更新
updateUser(id: Int!, name: String, email: String, age: Int): User

# ユーザー削除
deleteUser(id: Int!): Boolean!

# 投稿作成
createPost(userId: Int!, title: String!, content: String!): Post!
}
`;

// ========== リゾルバー(データ取得方法) ==========
const resolvers = {
Query: {
// 全ユーザー取得
users: () => users,

// 特定ユーザー取得
user: (parent, args) => {
return users.find(u => u.id === args.id);
},

// 全投稿取得
posts: () => posts,

// 特定投稿取得
post: (parent, args) => {
return posts.find(p => p.id === args.id);
}
},

Mutation: {
// ユーザー作成
createUser: (parent, args) => {
const newUser = {
id: users.length + 1,
name: args.name,
email: args.email,
age: args.age
};
users.push(newUser);
return newUser;
},

// ユーザー更新
updateUser: (parent, args) => {
const user = users.find(u => u.id === args.id);
if (!user) return null;

if (args.name) user.name = args.name;
if (args.email) user.email = args.email;
if (args.age) user.age = args.age;

return user;
},

// ユーザー削除
deleteUser: (parent, args) => {
const index = users.findIndex(u => u.id === args.id);
if (index === -1) return false;

users.splice(index, 1);
return true;
},

// 投稿作成
createPost: (parent, args) => {
const newPost = {
id: posts.length + 1,
userId: args.userId,
title: args.title,
content: args.content
};
posts.push(newPost);
return newPost;
}
},

// ========== リレーションリゾルバー ==========
User: {
// ユーザーの投稿取得
posts: (parent) => {
return posts.filter(p => p.userId === parent.id);
}
},

Post: {
// 投稿の作成者取得
author: (parent) => {
return users.find(u => u.id === parent.userId);
}
}
};

// ========== サーバー実行 ==========
const server = new ApolloServer({ typeDefs, resolvers });

server.listen().then(({ url }) => {
console.log(`GraphQLサーバー実行中: ${url}`);
});

GraphQLクライアント使用

// ========== GraphQLクエリ例 ==========

// 1. 名前のみ取得(Over-fetching防止)
query {
user(id: 1) {
name
}
}
// レスポンス: { "data": { "user": { "name": "田中太郎" } } }

// 2. ユーザーと投稿を一度に取得(Under-fetching防止)
query {
user(id: 1) {
name
email
posts {
title
content
}
}
}
// レスポンス:
{
"data": {
"user": {
"name": "田中太郎",
"email": "tanaka@example.com",
"posts": [
{ "title": "REST API入門", "content": "RESTは..." },
{ "title": "GraphQL入門", "content": "GraphQLは..." }
]
}
}
}

// 3. 複数のリソースを同時取得
query {
users {
name
}
posts {
title
}
}

// 4. 変数の使用
query GetUser($userId: Int!) {
user(id: $userId) {
name
email
}
}
// 変数: { "userId": 1 }

// ========== Mutation(データ変更) ==========

// 1. ユーザー作成
mutation {
createUser(name: "鈴木一郎", email: "suzuki@example.com", age: 28) {
id
name
email
}
}

// 2. ユーザー更新
mutation {
updateUser(id: 1, age: 26) {
id
name
age
}
}

// 3. ユーザー削除
mutation {
deleteUser(id: 1)
}

// ========== JavaScriptクライアント ==========
async function fetchGraphQL(query, variables = {}) {
const response = await fetch('http://localhost:4000/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query,
variables
})
});

return response.json();
}

// ユーザーと投稿を取得
const query = `
query GetUserWithPosts($userId: Int!) {
user(id: $userId) {
name
email
posts {
title
}
}
}
`;

const data = await fetchGraphQL(query, { userId: 1 });
console.log(data);

Apollo ClientでReact統合

// ========== Apollo Client設定 ==========
import { ApolloClient, InMemoryCache, ApolloProvider, useQuery, useMutation, gql } from '@apollo/client';

// クライアント作成
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
cache: new InMemoryCache()
});

// アプリにProviderを適用
function App() {
return (
<ApolloProvider client={client}>
<UserList />
</ApolloProvider>
);
}

// ========== クエリ使用 ==========
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
posts {
title
}
}
}
`;

function UserList() {
const { loading, error, data } = useQuery(GET_USERS);

if (loading) return <p>読み込み中...</p>;
if (error) return <p>エラー: {error.message}</p>;

return (
<div>
{data.users.map(user => (
<div key={user.id}>
<h3>{user.name}</h3>
<p>{user.email}</p>
<ul>
{user.posts.map(post => (
<li key={post.title}>{post.title}</li>
))}
</ul>
</div>
))}
</div>
);
}

// ========== Mutation使用 ==========
const CREATE_USER = gql`
mutation CreateUser($name: String!, $email: String!, $age: Int) {
createUser(name: $name, email: $email, age: $age) {
id
name
email
}
}
`;

function CreateUserForm() {
const [createUser, { data, loading, error }] = useMutation(CREATE_USER);

const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData(e.target);

createUser({
variables: {
name: formData.get('name'),
email: formData.get('email'),
age: parseInt(formData.get('age'))
}
});
};

if (loading) return <p>作成中...</p>;
if (error) return <p>エラー: {error.message}</p>;
if (data) return <p>作成完了: {data.createUser.name}</p>;

return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="名前" required />
<input name="email" type="email" placeholder="メール" required />
<input name="age" type="number" placeholder="年齢" />
<button type="submit">ユーザー作成</button>
</form>
);
}

// ========== キャッシングと最適化 ==========
const GET_USER = gql`
query GetUser($id: Int!) {
user(id: $id) {
id
name
email
}
}
`;

function UserProfile({ userId }) {
const { loading, error, data, refetch } = useQuery(GET_USER, {
variables: { id: userId },
// キャッシングポリシー
fetchPolicy: 'cache-first', // キャッシュ優先
// fetchPolicy: 'network-only', // 常にサーバーリクエスト
// fetchPolicy: 'cache-and-network', // キャッシュを表示して更新
});

// 手動リフレッシュ
const handleRefresh = () => {
refetch();
};

if (loading) return <p>読み込み中...</p>;
if (error) return <p>エラー: {error.message}</p>;

return (
<div>
<h2>{data.user.name}</h2>
<p>{data.user.email}</p>
<button onClick={handleRefresh}>リフレッシュ</button>
</div>
);
}

🤔 よくある質問

Q1. REST APIとGraphQLのどちらを選ぶべきですか?

A: プロジェクトの特性に応じて選択してください:

✅ REST APIを選択する場合:
├─ シンプルなCRUDアプリケーション
├─ リソース構造が明確
├─ キャッシングが重要(HTTPキャッシング活用)
├─ チームがRESTに慣れている
├─ ファイルアップロード/ダウンロードが多い
└─ 例: ブログ、Eコマース基本機能

✅ GraphQLを選択する場合:
├─ 複雑なデータリレーション
├─ モバイルアプリ(データ節約が重要)
├─ クライアントが多様(ウェブ、アプリ、タブレット)
├─ 迅速なフロントエンド開発を望む
├─ リアルタイムデータ購読が必要
└─ 例: ソーシャルメディア、ダッシュボード、リアルタイムアプリ

🤝 両方を一緒に使用することも可能:
├─ メインAPIはGraphQL
├─ ファイルアップロードはREST
└─ それぞれの長所を活用

Q2. パフォーマンスの違いは?

A:

// REST APIパフォーマンス特性
長所:
- HTTPキャッシング活用可能
GET /api/users/1
Cache-Control: max-age=3600

- CDNキャッシングが簡単
- シンプルなエンドポイントは高速

短所:
- Over-fetchingで不要なデータ転送
GET /api/users/1
すべてのフィールド返却(100KB)

- Under-fetchingで複数リクエスト必要
GET /api/users/1 // 1回目リクエスト
GET /api/users/1/posts // 2回目リクエスト
GET /api/posts/1/comments // 3回目リクエスト
// 合計3回のリクエスト!

// GraphQLパフォーマンス特性
長所:
- 正確に必要なデータのみ(10KB)
query {
user(id: 1) {
name
email
}
}

- 1回のリクエストですべてのデータ
query {
user(id: 1) {
name
posts {
title
comments { text }
}
}
}
// 1回のリクエストで完了!

短所:
- HTTPキャッシングが難しい(POSTリクエスト使用)
- 複雑なクエリはサーバー負担
- N+1問題(DataLoaderで解決)

実際のベンチマーク:
REST: 3回リクエスト、合計200KB、300ms
GraphQL: 1回リクエスト、合計50KB、150ms
→ モバイル環境でGraphQLが有利

Q3. GraphQLのN+1問題とは?

A: DataLoaderで解決できます:

// ========== N+1問題 ==========
// 10人のユーザーとそれぞれの投稿を取得

// 問題: 非効率的なクエリ
const resolvers = {
Query: {
users: () => {
return db.users.findAll(); // 1回のクエリ
}
},
User: {
posts: (user) => {
return db.posts.findByUserId(user.id); // ユーザーごとに1回!
}
}
};

// 合計クエリ: 1 + 10 = 11回
// 1回: ユーザー取得
// 10回: 各ユーザーの投稿取得

// ========== DataLoaderで解決 ==========
const DataLoader = require('dataloader');

// バッチで投稿を取得
const postLoader = new DataLoader(async (userIds) => {
// 一度にすべての投稿を取得
const posts = await db.posts.findByUserIds(userIds);

// ユーザー別にグループ化
const postsByUser = {};
posts.forEach(post => {
if (!postsByUser[post.userId]) {
postsByUser[post.userId] = [];
}
postsByUser[post.userId].push(post);
});

// 各ユーザーの投稿を返却
return userIds.map(id => postsByUser[id] || []);
});

const resolvers = {
User: {
posts: (user) => {
return postLoader.load(user.id); // バッチ処理!
}
}
};

// 合計クエリ: 1 + 1 = 2回
// 1回: ユーザー取得
// 1回: すべての投稿取得(バッチ)
// → パフォーマンス大幅向上!

// ========== 実戦例 ==========
const { ApolloServer } = require('apollo-server');
const DataLoader = require('dataloader');

const server = new ApolloServer({
typeDefs,
resolvers,
context: () => ({
// リクエストごとに新しいDataLoaderを作成
loaders: {
postLoader: new DataLoader(batchGetPosts),
userLoader: new DataLoader(batchGetUsers)
}
})
});

// リゾルバーで使用
const resolvers = {
User: {
posts: (user, args, context) => {
return context.loaders.postLoader.load(user.id);
}
}
};

Q4. REST APIバージョン管理 vs GraphQLスキーマ進化?

A:

// ========== REST APIバージョン管理 ==========

// 方法1: URLにバージョン含む
GET /api/v1/users
GET /api/v2/users // 新バージョン

// 方法2: ヘッダーにバージョン指定
GET /api/users
Accept: application/vnd.myapp.v1+json

// 問題:
// - 複数バージョンの保守
// - クライアント更新必要
// - 旧バージョンサポート負担

// 例: フィールド名変更
// v1
{
"name": "田中太郎"
}

// v2
{
"fullName": "田中太郎" // name → fullName
}

// ========== GraphQLスキーマ進化 ==========

// 追加は簡単
type User {
id: Int!
name: String!
email: String! # 既存
phone: String # 追加(新フィールド)
address: Address # 追加(新タイプ)
}

// 変更: @deprecatedを使用
type User {
id: Int!
name: String! @deprecated(reason: "Use fullName instead")
fullName: String! # 新フィールド
email: String!
}

// クライアントは段階的に更新
query {
user(id: 1) {
name # まだ動作する(deprecated)
fullName # 新フィールド使用
}
}

// 長所:
// - 単一のエンドポイント
// - バージョン管理不要
// - 段階的マイグレーション
// - クライアントが希望する時点で更新

Q5. リアルタイムデータはどう処理しますか?

A:

// ========== REST API: ポーリングまたはSSE ==========

// 1. ポーリング
setInterval(async () => {
const response = await fetch('/api/messages');
const messages = await response.json();
updateUI(messages);
}, 3000); // 3秒ごとにリクエスト
// 短所: 非効率的、サーバー負担

// 2. Server-Sent Events (SSE)
const eventSource = new EventSource('/api/messages/stream');
eventSource.onmessage = (event) => {
const message = JSON.parse(event.data);
updateUI(message);
};
// 短所: 一方向通信のみ可能

// ========== GraphQL: Subscription ==========

// サーバー設定
const { PubSub } = require('graphql-subscriptions');
const pubsub = new PubSub();

const typeDefs = gql`
type Message {
id: Int!
text: String!
userId: Int!
createdAt: String!
}

type Subscription {
messageAdded: Message!
}

type Mutation {
addMessage(text: String!): Message!
}
`;

const resolvers = {
Mutation: {
addMessage: (parent, args) => {
const message = {
id: messages.length + 1,
text: args.text,
userId: 1,
createdAt: new Date().toISOString()
};
messages.push(message);

// 購読者に通知
pubsub.publish('MESSAGE_ADDED', {
messageAdded: message
});

return message;
}
},

Subscription: {
messageAdded: {
subscribe: () => pubsub.asyncIterator(['MESSAGE_ADDED'])
}
}
};

// クライアント(React)
import { useSubscription, gql } from '@apollo/client';

const MESSAGE_SUBSCRIPTION = gql`
subscription OnMessageAdded {
messageAdded {
id
text
userId
createdAt
}
}
`;

function ChatRoom() {
const { data, loading } = useSubscription(MESSAGE_SUBSCRIPTION);

useEffect(() => {
if (data) {
console.log('新しいメッセージ:', data.messageAdded);
addMessageToUI(data.messageAdded);
}
}, [data]);

return <div>{/* UI */}</div>;
}

// 長所:
// - リアルタイム双方向通信
// - WebSocketベース
// - 効率的

🎓 次のステップ

REST APIとGraphQLを理解したら、次を学習してみましょう:

  1. APIとは? (ドキュメント作成予定) - API基本概念
  2. JWTトークン (ドキュメント作成予定) - API認証
  3. WebSocketとは? (ドキュメント作成予定) - リアルタイム通信

実習してみる

# ========== REST API実習(Express) ==========
mkdir rest-api-demo
cd rest-api-demo
npm init -y
npm install express

# server.js作成後
node server.js

# テスト
curl http://localhost:3000/api/users
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{"name":"田中太郎","email":"tanaka@example.com"}'

# ========== GraphQL実習(Apollo Server) ==========
mkdir graphql-demo
cd graphql-demo
npm init -y
npm install apollo-server graphql

# server.js作成後
node server.js

# GraphQL Playgroundを開く
# http://localhost:4000
# ブラウザでクエリ実行可能!

# ========== React + GraphQL ==========
npx create-react-app my-app
cd my-app
npm install @apollo/client graphql

# Apollo Client設定後実行
npm start

🎬 まとめ

REST APIとGraphQLはそれぞれ長所と短所があります:

  • REST API: シンプルで慣れていてキャッシングが簡単
  • GraphQL: 柔軟で効率的、リアルタイムサポート
  • 選択基準: プロジェクトの複雑度、チーム経験、パフォーマンス要件
  • ハイブリッド: 必要に応じて2つの方式を一緒に使用

正しいAPI設計で素晴らしいアプリケーションを作りましょう! 🔄