api-design について
REST、GraphQL、gRPC、リアルタイムプロトコルを扱う、API開発者向けの設計ガイドです。バージョン管理、セキュリティ、ドキュメント、設計上の注意点も取り上げます。
主な機能
- 01RESTのリソース命名とHTTPメソッド
- 02GraphQLのスキーマとリゾルバー設計
- 03gRPCとProtocol Buffers
- 04APIのバージョン管理と非推奨化
- 05リアルタイムAPIとセキュリティ対策
- 06OpenAPIドキュメントと設計チェック
活用シーン
- 01新しいAPIのパスやレスポンス形式の検討
- 02APIのバージョン変更時の互換性評価
- 03リアルタイム通信に使うプロトコルの選定
- 04入力検証やレート制限のリスク確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ miles990/ claude-software-skills --skill api-designインストール後、Claude Code で/api-designを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
templates/openapi.yamlREADME.mdschema.graphql詳細
API 設計
概要
直感的で一貫性があり、スケーラブルな API を構築するための設計原則です。REST、GraphQL、gRPC、リアルタイムプロトコルを扱います。
RESTful API 設計
リソースの命名
✅ Good (nouns, plural):
GET /users # List users
GET /users/123 # Get user
POST /users # Create user
PUT /users/123 # Update user
DELETE /users/123 # Delete user
❌ Bad (verbs, actions):
GET /getUsers
POST /createUser
POST /users/123/delete
ネストされたリソース
# Hierarchical relationship
GET /users/123/orders # User's orders
GET /users/123/orders/456 # Specific order
# Alternative: Query parameter for filtering
GET /orders?userId=123 # Filter orders by user
# Rule: Nest max 2 levels deep
❌ /users/123/orders/456/items/789/reviews
✅ /order-items/789/reviews
HTTP メソッドとステータスコード
| メソッド | 目的 | 成功 | エラー |
|---|---|---|---|
| GET | 読み取り | 200 | 404 |
| POST | 作成 | 201 | 400, 409 |
| PUT | 置換 | 200 | 400, 404 |
| PATCH | 部分更新 | 200 | 400, 404 |
| DELETE | 削除 | 204 | 404 |
// Response structure
interface ApiResponse<T> {
data: T;
meta?: {
page: number;
limit: number;
total: number;
};
}
interface ApiError {
error: {
code: string; // Machine-readable
message: string; // Human-readable
details?: object; // Validation errors, etc.
};
}
ページネーション
// Offset-based (simple, has issues with large datasets)
GET /users?page=2&limit=20
// Cursor-based (stable, performant)
GET /users?cursor=eyJpZCI6MTIzfQ&limit=20
// Response
{
"data": [...],
"meta": {
"nextCursor": "eyJpZCI6MTQzfQ",
"hasMore": true
}
}
フィルタリングとソート
// Query parameters
GET /products?category=electronics&minPrice=100&maxPrice=500
GET /products?sort=-createdAt,name // - prefix for descending
// Filter operators
GET /users?age[gte]=18&age[lte]=65
GET /users?status[in]=active,pending
GET /users?name[like]=john*
GraphQL 設計
スキーマ設計
type User {
id: ID!
email: String!
name: String!
posts(first: Int, after: String): PostConnection!
createdAt: DateTime!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
}
# Connections for pagination (Relay spec)
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
type PostEdge {
node: Post!
cursor: String!
}
type Query {
user(id: ID!): User
users(first: Int, after: String, filter: UserFilter): UserConnection!
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!
}
input CreateUserInput {
email: String!
name: String!
password: String!
}
type CreateUserPayload {
user: User
errors: [Error!]
}
リゾルバのパターン
// N+1 prevention with DataLoader
const userLoader = new DataLoader(async (ids: string[]) => {
const users = await db.users.findMany({ where: { id: { in: ids } } });
return ids.map(id => users.find(u => u.id === id));
});
const resolvers = {
Post: {
author: (post, _, { loaders }) => loaders.user.load(post.authorId),
},
};
// Field-level authorization
const resolvers = {
User: {
email: (user, _, { currentUser }) => {
if (currentUser.id !== user.id && !currentUser.isAdmin) {
return null; // Hide from other users
}
return user.email;
},
},
};
エラーハンドリング
// Union types for expected errors
type CreatePostResult = Post | ValidationError | NotAuthorizedError
// Or use errors field in payload
type CreatePostPayload {
post: Post
errors: [CreatePostError!]
}
union CreatePostError = ValidationError | RateLimitError
gRPC 設計
Protocol Buffers
syntax = "proto3";
package users.v1;
service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse);
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
// Streaming
rpc WatchUsers(WatchUsersRequest) returns (stream UserEvent);
rpc BatchCreateUsers(stream CreateUserRequest) returns (BatchCreateResponse);
}
message User {
string id = 1;
string email = 2;
string name = 3;
google.protobuf.Timestamp created_at = 4;
}
message GetUserRequest {
string id = 1;
}
message GetUserResponse {
User user = 1;
}
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
UserFilter filter = 3;
}
message ListUsersResponse {
repeated User users = 1;
string next_page_token = 2;
}
gRPC を使うべき場合
| ユースケース | REST | GraphQL | gRPC |
|---|---|---|---|
| パブリック API | ✅ | ✅ | ❌ |
| モバイルアプリ | ✅ | ✅ | ⚠️ |
| マイクロサービス | ⚠️ | ❌ | ✅ |
| リアルタイム | ❌ | ⚠️ | ✅ |
| ブラウザクライアント | ✅ | ✅ | ⚠️ |
API バージョニング
戦略
# URL versioning (most common)
GET /v1/users
GET /v2/users
# Header versioning
GET /users
Accept: application/vnd.api+json; version=2
# Query parameter
GET /users?version=2
破壊的変更と非破壊的変更
Non-Breaking (safe):
✅ Add new optional field
✅ Add new endpoint
✅ Add new optional query parameter
✅ Expand enum values (if client ignores unknown)
Breaking (requires new version):
❌ Remove field
❌ Rename field
❌ Change field type
❌ Make optional field required
❌ Change URL structure
非推奨化の戦略
// OpenAPI deprecation
/**
* @deprecated Use /v2/users instead. Will be removed on 2025-06-01.
*/
app.get('/v1/users', ...);
// Response header
res.setHeader('Deprecation', 'true');
res.setHeader('Sunset', 'Sat, 01 Jun 2025 00:00:00 GMT');
res.setHeader('Link', '</v2/users>; rel="successor-version"');
リアルタイム API
WebSocket
// Server
wss.on('connection', (ws) => {
ws.on('message', (data) => {
const message = JSON.parse(data);
switch (message.type) {
case 'subscribe':
subscriptions.add(ws, message.channel);
break;
case 'unsubscribe':
subscriptions.remove(ws, message.channel);
break;
}
});
});
// Client
const ws = new WebSocket('wss://api.example.com/ws');
ws.send(JSON.stringify({
type: 'subscribe',
channel: 'orders:user:123'
}));
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
handleUpdate(data);
};
Server-Sent Events (SSE)
// Server
app.get('/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const sendEvent = (data: object) => {
res.write(`data: ${JSON.stringify(data)}\n\n`);
};
// Subscribe to events
eventEmitter.on('update', sendEvent);
req.on('close', () => {
eventEmitter.off('update', sendEvent);
});
});
// Client
const source = new EventSource('/events');
source.onmessage = (event) => {
const data = JSON.parse(event.data);
handleUpdate(data);
};
API ドキュメント
OpenAPI (Swagger)
openapi: 3.0.3
info:
title: User API
version: 1.0.0
paths:
/users:
get:
summary: List users
parameters:
- name: limit
in: query
schema:
type: integer
default: 20
responses:
'200':
description: Success
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
required: [id, email, name]
properties:
id:
type: string
format: uuid
email:
type: string
format: email
name:
type: string
セキュリティのベストプラクティス
| プラクティス | 実装 |
|---|---|
| 認証 | ベアラートークン、API キー |
| レート制限 | X-RateLimit-* ヘッダー |
| 入力検証 | スキーマ検証 (Zod、Joi) |
| CORS | 許可するオリジンのホワイトリスト |
| HTTPS | 本番環境では常に使用 |
| リクエスト ID | トレース用の X-Request-ID |
// Rate limit headers
res.setHeader('X-RateLimit-Limit', '100');
res.setHeader('X-RateLimit-Remaining', '95');
res.setHeader('X-RateLimit-Reset', '1640000000');
関連スキル
- [[architecture-patterns]] - API ゲートウェイ、マイクロサービス
- [[security-practices]] - 認証、認可
- [[documentation]] - API ドキュメントツール
Sharp Edges(よくある落とし穴)
これらは API 設計において最もよくある、最もコストの高い間違いです
SE-1: 破壊的変更 (Breaking Changes)
- 深刻度: critical
- 状況: 適切なバージョニングなしに既存の API を変更し、クライアントがクラッシュする
- 原因: フィールドの削除、フィールド型の変更、必須かどうかの変更、名前の変更
- 症状:
- クライアントで突然エラーが発生する
- 旧バージョンのアプリが使用できなくなる
- ユーザーから「昨日は使えたのに、今日は壊れた」という苦情が来る
- 検出:
\-.*field|rename.*property|required.*true.*→.*false|type.*string.*→.*number - 解決策: API バージョニングを使用する、追加的な変更のみを行う、非推奨化の期限を設定する
SE-2: 過剰取得 (Over-fetching)
- 深刻度: medium
- 状況: API がクライアントにとって不要なデータを大量に返す
- 原因: 「とにかく全部返しておけばいい」という姿勢、異なるユースケースを考慮していない
- 症状:
- API レスポンスは大きいが、クライアントはそのごく一部しか使わない
- モバイルデバイスでの読み込みが遅い
- 帯域幅の無駄
- 検出:
select\s*\*|findMany\(\)|findAll\(\)(?!.*select) - 解決策: フィールド選択を使用する、GraphQL を使う、要件に応じてエンドポイントを設計する
SE-3: 取得不足 (Under-fetching)
- 深刻度: medium
- 状況: 完全なデータを取得するために複数の API を呼び出す必要がある
- 原因: エンドポイントを細分化しすぎている、一般的なユースケースを考慮していない
- 症状:
- フロントエンドが 1 ページをレンダリングするために 5 回以上の API 呼び出しを必要とする
- 複雑なフロントエンドのデータ統合ロジック
- N+1 API リクエストの問題
- 検出:
Promise\.all\(.*fetch.*fetch.*fetch|\.then\(.*fetch - 解決策: 集約エンドポイントを設計する、include/expand パラメータを使用する、BFF パターンを検討する
SE-4: 一貫性のないエラー形式
- 深刻度: high
- 状況: エンドポイントごとに異なる形式のエラーが返され、クライアントで統一的に処理するのが難しい
- 原因: 統一されたエラーハンドリングの規約がない、開発者ごとに独自に実装している
- 症状:
- あるエラーは
errorを使い、あるものはmessageを使い、あるものはerrorsを使う - HTTP ステータスコードの使用が一貫していない
- クライアントで異なるエラー形式を処理するために多くの if-else を書く必要がある
- あるエラーは
- 検出:
res\.json\(\{.*error|res\.json\(\{.*message|res\.status\(500\).*error - 解決策: 統一されたエラーレスポンス形式を定義する、グローバルエラーハンドラーを使用する、エラーコードシステムを構築する
SE-5: レート制限の欠如
- 深刻度: critical
- 状況: API にリクエスト頻度の制限がなく、悪用や攻撃を受けやすい
- 原因: 「まず作ってから考える」という姿勢、リスクを理解していない
- 症状:
- DDoS 攻撃によりサービスが停止する
- 単一のユーザーがすべてのリソースを消費する
- クラウドの請求額が爆発する
- 検出:
app\.use\((?!.*rateLimit)|router\.(?!.*limit)|express\(\)(?!.*rate) - 解決策: レート制限ミドルウェアを実装する、Redis でリクエストを追跡する、妥当な制限を設定する
バリデーション
V-1: HTTP ステータス 200 でエラーを返すことを禁止
- タイプ: regex
- 深刻度: high
- パターン:
res\.json\(\s*\{\s*error|res\.send\(\s*\{\s*error|\.json\(\{.*success:\s*false - メッセージ: Error responses should use appropriate HTTP status codes (4xx/5xx)
- 修正提案: Use
res.status(400).json({ error: ... })for client errors - 適用対象:
*.ts,*.js
V-2: API パスに動詞を使用
- タイプ: regex
- 重大度: medium
- パターン:
(get|create|update|delete|fetch|remove|add)\/|\/get|\/create|\/update|\/delete - メッセージ: REST API パスには動詞ではなく名詞を使用してください(代わりに HTTP メソッドを使用してください)
- 修正提案:
/getUsersをGET /usersに、/createUserをPOST /usersに変更してください - 適用:
*.ts,*.js
V-3: 入力検証の欠如
- タイプ: regex
- 重大度: critical
- パターン:
req\.body\.\w+(?!.*validate|.*schema|.*zod|.*joi|.*yup) - メッセージ: 明示的な検証なしでリクエストボディにアクセスしています
- 修正提案: リクエストデータを使用する前に Zod/Joi で検証を追加してください
- 適用:
*.ts,*.js
V-4: HTTP ステータスコードのハードコーディング
- タイプ: regex
- 重大度: low
- パターン:
res\.status\(\d{3}\) - メッセージ: 可読性のために名前付きステータスコードの使用を検討してください
- 修正提案: マジックナンバーの代わりに
HttpStatus.OKや定数を使用してください - 適用:
*.ts,*.js
V-5: API バージョン管理の欠如
- タイプ: regex
- 重大度: medium
- パターン:
app\.(get|post|put|delete)\s*\(\s*['"]\/(?!v\d|api\/v\d) - メッセージ: API ルートにバージョンプレフィックスがありません
- 修正提案: バージョンプレフィックスを追加してください:
/api/v1/users、またはヘッダーベースのバージョン管理を使用してください - 適用:
*.ts,*.js
コメント
Markdown 対応