rest-api について
REST API の設計と運用に取り組む開発者向けのリファレンススキルです。リソースの規約やステータスコードに加え、検証、ページネーション、セキュリティを扱います。
主な機能
- 01リソース命名と HTTP メソッド
- 02ステータスコードとレスポンス形式
- 03クエリパラメーターとページネーション
- 04検証、セキュリティ、API バージョニング
活用シーン
- 01一貫性のある REST エンドポイントの設計
- 02ページネーションと API バージョンの計画
- 03HTTP エラーや API 問題の診断
MCP 対応
より包括的な REST API ドキュメントを取得するための MCP ドキュメントツールに言及しています。
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill rest-apiインストール後、Claude Code で/rest-apiを入力して読み込まれたか確認します。
同梱ファイルquick-ref/conventions.mdquick-ref/error-handling.md
詳細
REST API の基本知識
詳しい知識:包括的なドキュメントについては、技術として
rest-apiを指定してmcp__documentation__fetch_docsを使用してください。
リソースの命名
# Collections (plural nouns)
GET /users # List users
POST /users # Create user
GET /users/{id} # Get user
PUT /users/{id} # Update user
DELETE /users/{id} # Delete user
# Nested resources
GET /users/{id}/posts # User's posts
POST /users/{id}/posts # Create user's post
# Avoid verbs in URLs
❌ GET /getUsers
❌ POST /createUser
✅ GET /users
✅ POST /users
HTTP メソッド
| メソッド | 目的 | 冪等性 |
|---|---|---|
| GET | リソースの読み取り | はい |
| POST | リソースの作成 | いいえ |
| PUT | リソースの置き換え | はい |
| PATCH | 部分更新 | はい |
| DELETE | リソースの削除 | はい |
ステータスコード
| コード | 意味 | 用途 |
|---|---|---|
| 200 | OK | GET/PUT/PATCH の成功 |
| 201 | 作成済み | POST の成功 |
| 204 | コンテンツなし | DELETE の成功 |
| 400 | 不正なリクエスト | 検証エラー |
| 401 | 認証されていません | 認証情報がない、または無効 |
| 403 | 禁止されています | 権限がありません |
| 404 | 見つかりません | リソースが存在しません |
| 409 | 競合 | 重複/競合 |
| 422 | 処理できません | 意味上のエラー |
| 500 | サーバーエラー | 内部エラー |
レスポンス形式
// Success
{
"data": { "id": 1, "name": "John" },
"meta": { "timestamp": "2024-01-15T10:00:00Z" }
}
// Collection
{
"data": [...],
"meta": { "total": 100, "page": 1, "limit": 20 }
}
// Error
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Email is required",
"details": [{ "field": "email", "message": "Required" }]
}
}
クエリパラメーター
GET /users?status=active&sort=-createdAt&page=1&limit=20
GET /users?fields=id,name,email
GET /users?include=posts,profile
GET /users?filter[age][gte]=18
このスキルを使用すべきでない場合
- GraphQL API の設計(
graphqlスキルを使用) - tRPC による型安全な API(
trpcスキルを使用) - OpenAPI 仕様の記述(
openapiスキルを使用) - WebSocket または SSE を必要とするリアルタイム API
- 複雑なネストされたクエリを必要とする API(GraphQL を検討)
アンチパターン
| アンチパターン | 問題点 | 解決策 |
|---|---|---|
URL に動詞を使用する(/getUser、/createOrder) | REST の原則に沿わず、リソース命名規則に違反します | 名詞に対して HTTP メソッドを使用します(GET /users、POST /orders) |
| エラーに対して 200 を返す | 誤解を招き、HTTP のセマンティクスに反します | 適切な 4xx/5xx ステータスコードを使用します |
| API のバージョン管理をしない | 破壊的変更がすべてのクライアントに影響します | URL またはヘッダーでバージョンを管理します(/v1/、/v2/) |
| データベース ID をそのまま公開する | セキュリティリスクとなり、実装が漏えいします | UUID または不透明な識別子を使用します |
| 大規模なコレクションにページネーションがない | パフォーマンスの問題やタイムアウトが発生します | カーソルまたはオフセット方式のページネーションを実装します |
| HTTP キャッシュヘッダーを無視する | パフォーマンスが低下し、不要な負荷が発生します | ETag、Cache-Control、Last-Modified を使用します |
| 状態を変更する操作に GET を使用する | セキュリティリスクとなり、REST の原則に反します | POST、PUT、PATCH、DELETE を使用します |
| レスポンス形式に一貫性がない | クライアントが混乱し、統合に問題が生じます | JSON エンベロープ形式に標準化します |
| レート制限がない | API の悪用やリソース枯渇につながります | 429 レスポンスを返すレート制限を実装します |
トラブルシューティングの概要
| 問題 | 考えられる原因 | 解決策 |
|---|---|---|
| 401 認証されていません | 認証トークンがない、または無効です | Authorization ヘッダーを確認し、トークンを検証します |
| 403 禁止されています | 認証は有効ですが、権限が不足しています | ユーザーのロールを確認し、アクセス制御を検証します |
| 404 見つかりません | リソースが存在しないか、パスが正しくありません | エンドポイント URL を確認し、リソース ID を確認します |
| 409 競合 | リソースがすでに存在するか、状態が競合しています | 一意性制約を確認し、冪等性を適切に処理します |
| 422 処理できないエンティティ | 検証に失敗しました | リクエストボディを確認し、スキーマに照らして検証します |
| 429 リクエストが多すぎます | レート制限を超過しました | バックオフを実装し、レート制限ヘッダーを確認します |
| 500 内部サーバーエラー | サーバー側のバグまたはクラッシュです | サーバーログを確認し、エラーハンドリングを追加します |
| CORS エラー | CORS ヘッダーがありません | 許可するオリジンを指定して CORS ミドルウェアを設定します |
| API のレスポンスが遅い | ページネーションがない、または N+1 クエリが発生しています | ページネーションを追加し、データベースクエリを最適化します |
本番環境への対応
セキュリティ設定
// Input validation and sanitization
import { z } from 'zod';
const CreateUserSchema = z.object({
name: z.string().min(1).max(100).trim(),
email: z.string().email().toLowerCase(),
age: z.number().int().min(0).max(150).optional(),
});
// Validate request body
app.post('/users', async (req, res) => {
const result = CreateUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: { code: 'VALIDATION_ERROR', details: result.error.issues },
});
}
// Use result.data (validated and transformed)
});
// SQL injection prevention - always use parameterized queries
// NEVER: `SELECT * FROM users WHERE id = '${userId}'`
// GOOD: Use ORM or prepared statements
レート制限
import rateLimit from 'express-rate-limit';
// Different limits for different endpoints
const standardLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
standardHeaders: true,
message: { error: { code: 'RATE_LIMIT_EXCEEDED' } },
});
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5, // Strict for auth endpoints
skipSuccessfulRequests: true,
});
app.use('/api', standardLimiter);
app.use('/api/auth/login', authLimiter);
API のバージョン管理
// URL versioning (recommended for breaking changes)
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
// Header versioning (for minor versions)
app.use('/api', (req, res, next) => {
const version = req.headers['api-version'] || '1.0';
req.apiVersion = version;
next();
});
// Sunset header for deprecated endpoints
app.get('/api/v1/legacy', (req, res) => {
res.set('Sunset', 'Sat, 01 Jun 2025 00:00:00 GMT');
res.set('Deprecation', 'true');
// ... handle request
});
ページネーション
// Cursor-based pagination (recommended for large datasets)
interface PaginatedResponse<T> {
data: T[];
meta: {
nextCursor: string | null;
hasMore: boolean;
};
}
app.get('/users', async (req, res) => {
const cursor = req.query.cursor as string | undefined;
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
const users = await db.users.findMany({
take: limit + 1, // Fetch one extra to check if there's more
cursor: cursor ? { id: cursor } : undefined,
orderBy: { createdAt: 'desc' },
});
const hasMore = users.length > limit;
const data = hasMore ? users.slice(0, -1) : users;
res.json({
data,
meta: {
nextCursor: hasMore ? data[data.length - 1].id : null,
hasMore,
},
});
});
エラーハンドリング
// Consistent error response format
interface APIError {
error: {
code: string;
message: string;
details?: unknown;
requestId?: string;
};
}
// Global error handler
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
const requestId = req.headers['x-request-id'] as string;
// Log error with context
logger.error({
err,
requestId,
path: req.path,
method: req.method,
});
// Don't leak internal errors
const statusCode = err instanceof HTTPError ? err.statusCode : 500;
const message = statusCode === 500 ? 'Internal Server Error' : err.message;
res.status(statusCode).json({
error: {
code: err.code || 'INTERNAL_ERROR',
message,
requestId,
},
});
});
監視メトリクス
| メトリクス | アラートしきい値 |
|---|---|
| リクエストレイテンシ p99 | > 500ms |
| エラー率(4xx) | > 5% |
| エラー率(5xx) | > 1% |
| レート制限のヒット数 | > 100/min |
| リクエストサイズ | > 10MB |
CORS 設定
import cors from 'cors';
const corsOptions = {
origin: (origin, callback) => {
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
credentials: true,
maxAge: 86400, // 24 hours
};
app.use(cors(corsOptions));
リクエスト/レスポンスのログ記録
import pino from 'pino-http';
app.use(pino({
redact: ['req.headers.authorization', 'req.body.password'],
serializers: {
req: (req) => ({
method: req.method,
url: req.url,
query: req.query,
}),
res: (res) => ({
statusCode: res.statusCode,
}),
},
}));
チェックリスト
- すべてのエンドポイントで入力を検証する
- レート制限を設定する
- CORS を適切に制限する
- API バージョニング戦略を策定する
- カーソルベースのページネーションを使用する
- エラー形式を統一する
- リクエスト ID を追跡する
- 構造化ログを記録する
- URL に機密データを含めない
- リクエストボディのサイズ制限を設定する
- タイムアウトを処理する
- ヘルスチェックエンドポイントを用意する
リファレンスドキュメント
コメント
Markdown 対応