優れた REST API の構築は、現代の開発者にとって最も重要なスキルの一つです。優れた API は直感的で、一貫性があり、安全で、メンテナンスしやすいものです。この包括的なガイドでは、URL 命名規則や HTTP メソッドから認証、ページネーション、レート制限、よくある間違いまで、すべてを網羅しています。
URL 命名規則
良い REST API の基盤は、適切に設計された URL から始まります。エンドポイントは予測可能で、読みやすく、一貫したパターンに従う必要があります。
動詞ではなく名詞を使用
REST リソースは名詞で識別されるべきです。HTTP メソッドがすでにアクションを示しているため、URL パスに動詞を含める必要はありません。
# Good - nouns as resources
GET /users # Get all users
GET /users/123 # Get user 123
POST /users # Create a new user
DELETE /users/123 # Delete user 123
# Bad - verbs in URLs
GET /getUsers
POST /createUser
DELETE /deleteUser/123コレクションには複数形を使用
コレクションエンドポイントには常に複数形の名詞を使用してください。これにより、コレクション全体を参照する場合でも、その中の単一リソースを参照する場合でも一貫性が保たれます。
# Good - consistent plurals
GET /users # Collection
GET /users/123 # Single resource in collection
GET /products # Collection
GET /products/456 # Single resource
# Bad - inconsistent or singular
GET /user # Ambiguous: one user or all users?
GET /user/123
GET /product-listリレーションシップにはネストされたリソース
ネストを使用してリソース間の関係を表現しますが、2レベルより深くならないようにしてください。
# Good - clear parent-child relationship (max 2 levels)
GET /users/123/orders # Orders belonging to user 123
GET /users/123/orders/456 # Specific order of user 123
# Bad - too deeply nested
GET /users/123/orders/456/items/789/reviews
# Better: use a direct resource endpoint
GET /order-items/789/reviews
# Or: flatten with query parameters
GET /reviews?order_item_id=789URL パターン:良い例 vs 悪い例
| 良い | 悪い | 理由 |
|---|---|---|
| GET /users | GET /getUsers | HTTP method already implies the action |
| GET /users/123 | GET /user?id=123 | Use path parameters for resource identity |
| POST /users | POST /createUser | POST already means create |
| PUT /users/123 | POST /updateUser | PUT/PATCH for updates, use resource path |
| DELETE /users/123 | POST /deleteUser | Use DELETE method, not POST with verb |
| GET /users/123/orders | GET /getUserOrders?userId=123 | Use nested resources for relationships |
| GET /products?category=electronics | GET /electronics/products | Use query params for filtering |
| GET /users?status=active&sort=name | GET /active-users-sorted-by-name | Use query params, not encoded URLs |
| PATCH /users/123 | POST /users/123/update | Use HTTP methods, not URL verbs |
| GET /users/123/avatar | GET /getAvatarForUser/123 | Keep it resource-oriented |
HTTP メソッド
各 HTTP メソッドには特定のセマンティクスがあります。正しく使用することで、API が予測可能になります。
| メソッド | 目的 | 冪等性 | リクエストボディ | 例 |
|---|---|---|---|---|
| GET | Retrieve a resource or collection | Yes | No | GET /users/123 |
| POST | Create a new resource | No | Yes | POST /users |
| PUT | Replace a resource entirely | Yes | Yes | PUT /users/123 |
| PATCH | Partially update a resource | No* | Yes | PATCH /users/123 |
| DELETE | Remove a resource | Yes | Optional | DELETE /users/123 |
* PATCH can be idempotent depending on implementation. If the patch describes the final state (e.g., {"name": "John"}), it is idempotent. If it describes an operation (e.g., {"op": "increment", "path": "/count"}), it is not.
HTTP ステータスコード
正しいステータスコードを使用することで、クライアントがレスポンスを適切に処理できるようになります。
| コード | 名前 | 使用するタイミング |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH, or DELETE. Return the resource or confirmation. |
| 201 | Created | Successful POST that created a new resource. Include Location header with the new resource URL. |
| 204 | No Content | Successful DELETE or PUT/PATCH when no response body is needed. |
| 400 | Bad Request | The request is malformed — invalid JSON, missing required fields, wrong data types. |
| 401 | Unauthorized | No valid authentication credentials provided. The client must authenticate first. |
| 403 | Forbidden | The client is authenticated but does not have permission for this action. |
| 404 | Not Found | The requested resource does not exist. Also use for hidden resources (instead of 403) for security. |
| 409 | Conflict | The request conflicts with the current state — duplicate email, version mismatch, etc. |
| 422 | Unprocessable Entity | The request is well-formed but fails validation — email format invalid, password too short. |
| 429 | Too Many Requests | Rate limit exceeded. Include Retry-After header with seconds until the client can retry. |
| 500 | Internal Server Error | An unexpected server error occurred. Log the details server-side but do not expose them to clients. |
| 503 | Service Unavailable | The server is temporarily unavailable — maintenance mode, overloaded. Include Retry-After header. |
エラーハンドリング
一貫したエラーレスポンスにより、API が使いやすくなります。すべてのエラーは構造化された JSON ボディを返す必要があります。
標準エラーレスポンス形式
すべてのエラーレスポンスに一貫したエラーエンベロープを使用してください。
// Standard error response
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "The user with ID 123 was not found.",
"status": 404,
"timestamp": "2026-01-15T10:30:00Z",
"request_id": "req_abc123def456",
"documentation_url": "https://api.example.com/docs/errors#RESOURCE_NOT_FOUND"
}
}
// Another example: authentication error
{
"error": {
"code": "TOKEN_EXPIRED",
"message": "The access token has expired. Please refresh your token.",
"status": 401,
"timestamp": "2026-01-15T10:30:00Z",
"request_id": "req_xyz789"
}
}フィールドレベルのバリデーションエラー
422 バリデーションエラーには、フィールドレベルのエラー情報を含めてください。
// 422 Validation error with field-level details
{
"error": {
"code": "VALIDATION_FAILED",
"message": "The request body contains invalid fields.",
"status": 422,
"details": [
{
"field": "email",
"message": "Must be a valid email address.",
"rejected_value": "not-an-email"
},
{
"field": "password",
"message": "Must be at least 8 characters long.",
"rejected_value": "short"
},
{
"field": "age",
"message": "Must be a positive integer.",
"rejected_value": -5
}
],
"timestamp": "2026-01-15T10:30:00Z",
"request_id": "req_val456"
}
}認証と認可
適切な認証戦略の選択はユースケースによって異なります。最も一般的な3つのアプローチの比較です。
API キー
ヘッダーやクエリパラメータで渡されるシンプルな文字列トークン。サーバー間通信に最適です。
# API Key in header (recommended)
GET /api/users HTTP/1.1
Host: api.example.com
X-API-Key: sk_live_abc123def456ghi789
# API Key in query parameter (less secure - visible in logs)
GET /api/users?api_key=sk_live_abc123def456ghi789JWT Bearer トークン
ユーザークレームを含む自己完結型トークン。マイクロサービスでのステートレス認証に最適です。
# JWT Bearer Token in Authorization header
GET /api/users/me HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
# Token refresh flow
POST /api/auth/refresh HTTP/1.1
Content-Type: application/json
{
"refresh_token": "rt_abc123def456"
}
# Response
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "rt_new789xyz"
}OAuth 2.0
委任認可の業界標準。サードパーティアプリケーションがユーザーの代わりにリソースにアクセスできるようにします。
# OAuth 2.0 Authorization Code Flow
# Step 1: Redirect user to authorization server
GET https://auth.example.com/authorize?
response_type=code&
client_id=your_client_id&
redirect_uri=https://yourapp.com/callback&
scope=read:users write:users&
state=random_csrf_token
# Step 2: Exchange authorization code for tokens
POST https://auth.example.com/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTH_CODE_FROM_CALLBACK&
client_id=your_client_id&
client_secret=your_client_secret&
redirect_uri=https://yourapp.com/callback
# Step 3: Use the access token
GET /api/users/me HTTP/1.1
Authorization: Bearer ACCESS_TOKEN_HERE認証方式の比較
| 機能 | API Key | JWT | OAuth 2.0 |
|---|---|---|---|
| Complexity | Low | Medium | High |
| Stateless | Yes | Yes | Depends |
| User context | No | Yes | Yes |
| Token expiration | Manual rotation | Built-in (exp claim) | Built-in |
| Scope/permissions | Limited | Via claims | Fine-grained scopes |
| Third-party access | No | No | Yes (delegated) |
| Best for | Server-to-server | SPAs, Mobile apps | Third-party integrations |
| Revocation | Delete key | Blacklist needed | Revoke at auth server |
ページネーションパターン
リソースのリストを返すすべてのエンドポイントはページネーションをサポートする必要があります。
オフセットベースのページネーション
最もシンプルなアプローチ。ページ番号とリミットパラメータを使用します。
# Offset-based pagination request
GET /api/users?page=2&limit=20
# How it works internally
SELECT * FROM users
ORDER BY created_at DESC
LIMIT 20 OFFSET 20; -- Skip first 20, return next 20
# Pros: Simple, allows jumping to any page
# Cons: Inconsistent with concurrent modifications,
# slow on large offsets (OFFSET 100000)カーソルベースのページネーション
不透明なカーソルトークンを使用してデータセット内の位置をマークします。リアルタイムデータに適しています。
# Cursor-based pagination request
GET /api/users?cursor=eyJpZCI6MTIzfQ&limit=20
# The cursor is an opaque token (often base64-encoded)
# that points to a specific position in the dataset
# How it works internally (cursor decodes to {"id": 123})
SELECT * FROM users
WHERE id > 123
ORDER BY id ASC
LIMIT 20;
# Pros: Consistent results, fast on large datasets
# Cons: Cannot jump to arbitrary pages,
# only next/previous navigationページネーションレスポンス形式
すべてのページネーションレスポンスにメタデータを含めてください。
// Offset-based pagination response
{
"data": [
{ "id": 21, "name": "Alice" },
{ "id": 22, "name": "Bob" }
// ... 18 more items
],
"meta": {
"current_page": 2,
"per_page": 20,
"total_pages": 15,
"total_count": 294
},
"links": {
"self": "/api/users?page=2&limit=20",
"first": "/api/users?page=1&limit=20",
"prev": "/api/users?page=1&limit=20",
"next": "/api/users?page=3&limit=20",
"last": "/api/users?page=15&limit=20"
}
}
// Cursor-based pagination response
{
"data": [
{ "id": 124, "name": "Charlie" },
{ "id": 125, "name": "Diana" }
// ... 18 more items
],
"meta": {
"has_more": true,
"next_cursor": "eyJpZCI6MTQzfQ",
"prev_cursor": "eyJpZCI6MTI0fQ"
},
"links": {
"next": "/api/users?cursor=eyJpZCI6MTQzfQ&limit=20",
"prev": "/api/users?cursor=eyJpZCI6MTI0fQ&limit=20&direction=prev"
}
}API バージョニング戦略
API は時間とともに進化します。バージョニングにより、既存のクライアントを中断することなく破壊的変更を行えます。
| 戦略 | 例 | 利点 | 欠点 |
|---|---|---|---|
| URL Path | /v1/users, /v2/users | Explicit, easy to route and cache, clear in logs and docs | URL pollution, harder to sunset old versions |
| Custom Header | Accept-Version: v2 or X-API-Version: 2 | Clean URLs, version hidden from casual users | Harder to test (need tools to set headers), not cacheable by default |
| Query Parameter | /users?version=2 | Easy to test in browser, no URL path changes | Can be accidentally omitted, harder to route, pollutes query string |
| Content Negotiation | Accept: application/vnd.api.v2+json | Most RESTful approach, per-resource versioning | Complex, hard to test, poorly supported by many tools |
# URL Path versioning (recommended)
GET /v1/users/123 # Version 1
GET /v2/users/123 # Version 2 with breaking changes
# Express.js router example
const v1Router = express.Router();
const v2Router = express.Router();
v1Router.get('/users/:id', v1UserController.get);
v2Router.get('/users/:id', v2UserController.get);
app.use('/v1', v1Router);
app.use('/v2', v2Router);レート制限
レート制限は API を乱用から保護し、すべてのクライアントの公平な使用を確保します。
レート制限ヘッダー
クライアントが使用状況を監視できるよう、すべてのレスポンスにこれらのヘッダーを含めてください。
HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 1000 # Max requests per window
X-RateLimit-Remaining: 847 # Remaining requests in window
X-RateLimit-Reset: 1706176800 # Unix timestamp when window resets
X-RateLimit-Window: 3600 # Window duration in seconds (1 hour)
# Some APIs also include:
RateLimit-Policy: 1000;w=3600 # IETF draft standard format429 リクエスト過多レスポンス
クライアントがレート制限を超えた場合、Retry-After ヘッダー付きの 429 ステータスを返します。
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 45
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1706176800
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "You have exceeded the rate limit of 1000 requests per hour.",
"status": 429,
"retry_after": 45,
"documentation_url": "https://api.example.com/docs/rate-limiting"
}
}バックオフ戦略
クライアントは 429 レスポンスを受け取った際、指数バックオフを実装する必要があります。
// Exponential backoff with jitter
async function fetchWithRetry(url, options, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status !== 429) {
return response;
}
// Use Retry-After header if available
const retryAfter = response.headers.get('Retry-After');
let delay;
if (retryAfter) {
delay = parseInt(retryAfter, 10) * 1000;
} else {
// Exponential backoff: 1s, 2s, 4s, 8s, 16s
const baseDelay = Math.pow(2, attempt) * 1000;
// Add jitter (random 0-1s) to prevent thundering herd
const jitter = Math.random() * 1000;
delay = baseDelay + jitter;
}
console.log(`Rate limited. Retrying in ${delay}ms (attempt ${attempt + 1})`);
await new Promise(resolve => setTimeout(resolve, delay));
}
throw new Error('Max retries exceeded');
}セキュリティヘッダー
セキュリティヘッダーは API を一般的な Web 脆弱性から保護します。
| ヘッダー | 推奨値 | 目的 |
|---|---|---|
| Access-Control-Allow-Origin | https://yourapp.com (specific origin) | CORS: Controls which domains can call your API. Never use * in production with credentials. |
| Access-Control-Allow-Methods | GET, POST, PUT, PATCH, DELETE | CORS: Specifies which HTTP methods are allowed for cross-origin requests. |
| Access-Control-Allow-Headers | Authorization, Content-Type | CORS: Specifies which request headers are allowed in cross-origin requests. |
| Strict-Transport-Security | max-age=31536000; includeSubDomains | HSTS: Forces browsers to use HTTPS for all future requests to your domain. |
| Content-Security-Policy | default-src 'none'; frame-ancestors 'none' | CSP: Prevents XSS and data injection attacks. For APIs, restrict everything. |
| X-Content-Type-Options | nosniff | Prevents browsers from MIME-type sniffing. Ensures responses are treated as their declared content type. |
| X-Frame-Options | DENY | Prevents your API responses from being embedded in iframes (clickjacking protection). |
| Cache-Control | no-store, no-cache, must-revalidate | Prevents caching of sensitive API responses. Adjust per endpoint as needed. |
// Express.js security headers middleware
const helmet = require('helmet');
app.use(helmet());
// Custom CORS configuration
const cors = require('cors');
app.use(cors({
origin: ['https://yourapp.com', 'https://admin.yourapp.com'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Authorization', 'Content-Type'],
credentials: true,
maxAge: 86400 // Cache preflight for 24 hours
}));
// Additional security headers
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('Cache-Control', 'no-store');
next();
});OpenAPI による API ドキュメント
良いドキュメントは API の採用に不可欠です。OpenAPI は REST API を記述するための標準です。
- まず OpenAPI 仕様を書き(デザインファーストアプローチ)、次に仕様に合わせて API を実装します。
- すべてのエンドポイント、パラメータ、レスポンススキーマに詳細な説明を含めてください。
- すべてのリクエストとレスポンスボディにリアルな例の値を提供してください。
- すべてのエンドポイントのエラーレスポンスを文書化してください。
- タグを使用して関連エンドポイントをグループ化してください。
- OpenAPI 仕様を API コードと一緒にバージョン管理してください。
- インタラクティブなドキュメント(Swagger UI または Redoc)を設定してください。
# OpenAPI 3.1 specification example
openapi: 3.1.0
info:
title: User Management API
version: 1.0.0
description: API for managing users and their resources
servers:
- url: https://api.example.com/v1
paths:
/users:
get:
summary: List all users
tags: [Users]
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
'200':
description: A paginated list of users
content:
application/json:
schema:
$ref: '#/components/schemas/UserListResponse'
example:
data:
- id: 1
name: "Alice Johnson"
email: "alice@example.com"
meta:
current_page: 1
total_pages: 10
'401':
$ref: '#/components/responses/Unauthorized'
post:
summary: Create a new user
tags: [Users]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
example:
name: "Bob Smith"
email: "bob@example.com"
password: "securePassword123"
responses:
'201':
description: User created successfully
'422':
$ref: '#/components/responses/ValidationError'よくある REST API の間違い
経験豊富な開発者でもこれらの間違いを犯します。API がベストプラクティスに従っていることを確認してください。
| # | 間違い | なぜ問題か | 修正方法 |
|---|---|---|---|
| 1 | Using verbs in URLs | REST is resource-oriented; HTTP methods express actions | Use nouns: /users not /getUsers |
| 2 | Returning 200 for errors | Clients rely on status codes for error handling | Use proper status codes: 400, 404, 422, 500 |
| 3 | Not paginating list endpoints | Returns grow unbounded, causing timeouts and memory issues | Always paginate with sensible defaults (limit=20) |
| 4 | Exposing internal errors | Stack traces reveal implementation details to attackers | Log details server-side, return generic error messages |
| 5 | Ignoring CORS headers | Browser-based clients cannot call your API | Configure CORS for allowed origins and methods |
| 6 | Not versioning the API | Breaking changes affect all clients simultaneously | Use URL path versioning: /v1/users |
| 7 | Using POST for everything | Loses HTTP method semantics, breaks caching and idempotency | Use GET, POST, PUT, PATCH, DELETE appropriately |
| 8 | Inconsistent naming conventions | Mixed case or formats confuse API consumers | Pick one (snake_case or camelCase) and be consistent |
| 9 | No rate limiting | API is vulnerable to abuse and DDoS attacks | Implement rate limiting with clear headers |
| 10 | Missing Content-Type header | Clients cannot parse responses correctly | Always set Content-Type: application/json |
よくある質問
REST と RESTful の違いは何ですか?
REST は Roy Fielding が定義したアーキテクチャスタイルです。RESTful は REST 制約に準拠する API を表す形容詞です。実際にはほとんどの「REST API」はすべての REST 制約に厳密に従っていませんが、用語は互換的に使われます。
リソースの更新には PUT と PATCH のどちらを使うべきですか?
クライアントがリソースの完全な置換を送信する場合は PUT を使用します。変更されたフィールドのみを送信する部分更新には PATCH を使用します。
API バージョニングはどのように処理すべきですか?
URL パスバージョニング(例:/v1/users)が最も一般的で明示的なアプローチです。理解しやすく、ルーティングやキャッシュも容易です。
REST API での認証を処理する最良の方法は何ですか?
ほとんどのアプリケーションでは、JWT Bearer トークンがセキュリティとシンプルさの良いバランスを提供します。短期アクセストークンとリフレッシュトークンローテーションを使用してください。
REST API でネストされたリソースをどのように設計すべきですか?
ネストを使用して親子関係を示します:/users/123/orders。ネストは最大2レベルまでに制限してください。
REST API レスポンスは null フィールドを含めるべきですか、省略すべきですか?
契約によります。null フィールドを含めるとスキーマが予測可能になります。省略するとペイロードサイズが減ります。一貫性を保ち、ドキュメント化してください。