【現場完全ガイド】Laravelでつくる堅牢なAPIプラットフォーム――REST/JSON:API/GraphQL、OpenAPI、バージョニング、ETag/条件付きリクエスト、レート制限、idempotency、エラー設計、アクセシブルなドキュメント
この記事で学べること
- REST・JSON:API・GraphQL の選び方と Laravel での実装指針
- OpenAPI(OAS)による仕様駆動開発とリクエスト/レスポンスの検証
- バージョニング(URI/ヘッダー/サブドメイン)、非互換変更の移行戦略
- ETag/If-None-Match・If-Modified-Since によるキャッシュと帯域削減
- レート制限・idempotency・署名付き Webhook など実務の信頼性向上テクニック
- エラーの表現(RFC 7807 problem+json)・多言語メッセージ・トレースID
- APIドキュメント/サンプル/SDK を誰にでも読みやすくするアクセシビリティ設計
- テスト(Feature/Contract)・モニタリング・互換性チェックの運用
想定読者
- Laravel 初〜中級のバックエンドエンジニア:APIを安全に公開し、破壊的変更を避けたい方
- テックリード/PM:OpenAPI を中心にした仕様駆動とリリース管理を回したい方
- QA/ドキュメント担当:API仕様・チュートリアル・サンプルをアクセシブルに届けたい方
- 他サービス連携の開発者:Webhook/署名検証/idempotency の標準を整えたい方
アクセシビリティレベル:★★★★★
API自体の可用性だけでなく、ドキュメント・チュートリアル・サンプルコードを誰でも理解しやすい形で提供する方針を具体化します。色に依存しない表現、読み上げやコントラスト配慮、キーボード操作のみで完遂できるナビゲーション、サンプルのコピー容易性、図の代替テキストなどを含みます。
1. はじめに:APIは「契約」であり「公共物」
API は単なるエンドポイントではなく、長期にわたりクライアントと合意を守る「契約」です。破壊的変更や曖昧な解釈は、障害やサポート負荷として跳ね返ります。Laravel は認証・認可・ルーティング・バリデーション・キャッシュ・レート制限など API に必要な装置を一通り備えていますが、設計原則が無ければ活きません。この記事では、仕様駆動開発と互換性を軸に、堅牢で読みやすい API プラットフォームを段階的に組み立てます。
2. スタイルの選択:REST / JSON:API / GraphQL
2.1 ざっくり比較
- REST(一般的なJSON)
- 長所:学習コストが低く、HTTPの意味論に忠実。CDN/キャッシュが効きやすい。
- 短所:表現の自由度が高く、実装ごとにバラつきが出やすい。
- JSON:API(仕様準拠のREST)
- 長所:ドキュメント/エラー/関連取得/ページングの約束が明確。クライアント実装が楽。
- 短所:厳格ゆえに最初の学習コストがある。
- GraphQL
- 長所:過不足問題(over/under-fetch)を解消。複雑な画面に強い。
- 短所:HTTP キャッシュが効きにくい。N+1やスキーマの保守が難しくなりがち。
小規模〜標準的な業務APIは REST(または JSON:API)で十分です。ダッシュボードの複雑な集約やモバイル画面の最適化が課題なら GraphQL を検討します。混在させると運用が複雑になるため、まずは REST をベースに共通原則(エラー・認証・ページング)を固めることをおすすめします。
3. 仕様駆動開発:OpenAPI を中心に
3.1 OAS を単一の真実にする
OpenAPI Specification(OAS)を単一の真実として扱い、サーバ・クライアント・テスト・ドキュメントをそこから生成/検証します。メリットは以下です。
- 要件の曖昧さが減る
- 破壊的変更の検出が容易
- SDK/型生成でクライアントの安全性が上がる
3.2 ディレクトリ例
api/
├─ openapi.yaml # 仕様(人手で管理+CIで検証)
├─ examples/ # リクエスト/レスポンスの実例
└─ schemas/ # JSON Schema(再利用)
app/
└─ Http/
├─ Controllers/Api/
├─ Middleware/
└─ Requests/Api/
tests/
└─ Contract/ # OASとの契約テスト
3.3 Laravelでの型安全と検証
FormRequestに OAS と同じ制約を書く- さらに
league/openapi-psr7-validator等で実レスポンスが仕様に適合するかを契約テストで確認 spatie/laravel-data等で DTO を活用し、レスポンス整形を型で守る
4. バージョニングと互換性の約束
4.1 方式の比較
- URI プレフィックス:
/api/v1/...が最も分かりやすく、キャッシュ/ルーティングも簡単 - ヘッダー(
Accept: application/vnd.example.v2+json):エレガントだが学習コストあり - サブドメイン:
v2.api.example.comは API ゲートウェイと相性が良い
まずは /api/v1 を推奨。互換性を壊す変更は v2 で提供し、v1 は一定期間並行稼働します。
4.2 変更の分類と運用
- 非破壊:フィールド追加、デフォルト拡張、ページサイズ上限の引き上げ
- 準破壊:デフォルト値変更、並び順変更(通知が必要)
- 破壊:フィールド削除、型変更、意味変更、エンドポイント削除
破壊的変更は避け、追加で表現できないか考えるのが原則です。どうしても必要なら、新バージョンを公開し、移行ガイドとリンター(互換チェック)を用意します。
5. 認証・認可・スコープ
5.1 認証方式
- セッション+CSRF:同一オリジンのSPA向け
- Sanctum のパーソナルアクセストークン:外部クライアント/API向け
- OAuth 2.1 / OIDC(外部IdP):パートナー連携やSaaS統合向け
5.2 スコープ(abilities)
// 発行
$token = $user->createToken('cli', ['orders:read','orders:write'])->plainTextToken;
// 検証
abort_unless($request->user()->tokenCan('orders:write'), 403);
スコープを細かく切るほどセキュリティは上がりますが、運用が複雑になります。まずは「読み取り」「書き込み」の2分割から始め、必要に応じて拡張します。
6. ルーティング・命名・ページング・ソート
6.1 命名
- 複数形のリソース:
GET /orders、GET /orders/{id}、POST /orders - 動詞はサブリソース:
POST /orders/{id}/cancelのように意味を明示 - 一貫性が最重要。短くて具体的に
6.2 ページング・ソート・フィルタ
GET /orders?page=2&per_page=50&sort=-created_at&status=shipped
per_pageは上限を設ける(例:100)- ソートは列挙で許可。任意列は拒否
- ページング情報はレスポンスに含める
{
"data": [ ... ],
"meta": { "total": 1234, "page": 2, "per_page": 50 },
"links": { "next": "...", "prev": "..." }
}
7. 条件付きリクエストとHTTPキャッシュ
7.1 ETag / If-None-Match
各リソースの表現にハッシュ(ETag)を付与し、クライアントが If-None-Match を送ると、変更がなければ 304 Not Modified を返します。
public function show(Order $order) {
$etag = sha1($order->updated_at.$order->id);
if (request()->header('If-None-Match') === $etag) {
return response()->noContent(304);
}
return response()->json($order)->header('ETag', $etag);
}
7.2 Last-Modified / If-Modified-Since
時刻ベースの条件付きも役立ちます。帯域削減と「無駄な再取得」の抑止は、コストとレイテンシの両方を下げます。
8. レート制限・idempotency・リトライ
8.1 レート制限
// routes/api.php
Route::middleware('throttle:60,1')->group(function(){
Route::get('/orders', [OrderController::class,'index']);
});
- APIキーやユーザーIDを「by」に使い分けるとフェアに
429 Too Many RequestsとRetry-Afterを返す
8.2 idempotency(重複防止)
決済や作成系は冪等性キーで二重実行を防ぎます。
// ミドルウェアの概略
$key = request()->header('Idempotency-Key');
abort_unless($key, 400);
$lock = Cache::lock("idem:$key", 60);
abort_unless($lock->get(), 409); // 進行中
try {
// 同一キーの既存レスポンスがあれば再送
if ($cached = Cache::get("idem:resp:$key")) return response()->json($cached['body'], $cached['status']);
$resp = $next($request);
Cache::put("idem:resp:$key", ['status'=>$resp->status(), 'body'=>json_decode($resp->getContent(),true)], 3600);
return $resp;
} finally { optional($lock)->release(); }
8.3 リトライ方針
HTTPクライアントからのリトライは指数バックオフ(例:100ms, 200ms, 400ms, …)。サーバ側は安全な再実行を可能にします。
9. エラー設計:RFC 7807(problem+json)で統一
9.1 フォーマット
{
"type": "https://docs.example.com/problems/validation",
"title": "Validation Failed",
"status": 422,
"detail": "email は必須です。",
"instance": "/api/v1/users",
"errors": {
"email": ["この項目は必須です。"]
},
"trace_id": "req-8a2c..."
}
typeはドキュメントのURL(固定ページ)にして再現手順や対処を明記trace_idを必ず付け、問い合わせやログ探索を短時間で
9.2 Laravel での実装
例外ハンドラで Throwable を捕まえ、ステータスコード別に problem+json で返す。FormRequest からのバリデーションエラーも統一します。
// app/Exceptions/Handler.php(一部)
public function render($request, Throwable $e)
{
if ($request->expectsJson()) {
$traceId = (string) Str::uuid();
Log::error('api.error', ['trace_id'=>$traceId, 'ex'=>$e]);
$status = $this->statusOf($e); // 例外から適切なHTTPコードへ
$payload = [
'type' => $this->problemType($e),
'title' => Response::$statusTexts[$status] ?? 'Error',
'status' => $status,
'detail' => $this->detailOf($e),
'instance' => $request->path(),
'trace_id' => $traceId,
];
if ($e instanceof ValidationException) {
$payload['type'] = 'https://docs.example.com/problems/validation';
$payload['errors'] = $e->errors();
}
return response()->json($payload, $status)->header('Content-Type','application/problem+json');
}
return parent::render($request, $e);
}
9.3 多言語メッセージ
detail は Accept-Language を尊重してローカライズ。ただし、機械可読な type と status は変えません。英語版の detail も別途入手できるとサポートが楽になります。
10. JSON セキュア化とサイズ管理
application/jsonを使用し、JSONP は禁止- 出力は最小限のフィールド。可観測性のための内部情報(スタックトレースやPII)は返さない
- 一覧はページング必須。
per_pageの上限で帯域コントロール - 大きな配列はストリーミングか非同期エクスポートに逃がす
11. Webhook:署名検証とリプレイ対策
11.1 送信側(こちらが打つ)
- ヘッダーに署名と時刻を入れる
- 冪等性キー(イベントID)を付与
$payload = json_encode($event, JSON_UNESCAPED_UNICODE);
$ts = time();
$sig = hash_hmac('sha256', $ts.'.'.$payload, config('services.webhook.secret'));
Http::withHeaders([
'X-Webhook-Timestamp' => $ts,
'X-Webhook-Signature' => $sig,
'Idempotency-Key' => $event['id'],
])->post($url, $event);
11.2 受信側(外部から受ける)
- まず署名検証、時刻の許容誤差(例:±5分)
- 受信後はジョブに投げて非同期処理、同一イベントIDは無視
$payload = $request->getContent();
$ts = $request->header('X-Webhook-Timestamp');
$sig = $request->header('X-Webhook-Signature');
abort_if(abs(time() - (int)$ts) > 300, 401);
$calc = hash_hmac('sha256', $ts.'.'.$payload, config('services.webhook.secret'));
abort_unless(hash_equals($calc, $sig), 401);
$event = json_decode($payload, true);
if (Cache::add('evt:'.$event['id'], true, 3600)) {
dispatch(new HandleWebhook($event));
}
12. ドキュメントのアクセシビリティ:読みやすく、探しやすく、試しやすく
12.1 情報設計
- 左カラムに目次(キーボード操作で展開可能)
- 1ページ1テーマ。最初に「できること」「対象読者」「最短手順」を提示
- 背景色やバッジ色に頼らず、テキストとアイコンで状態を表現
- 図・シーケンスには代替テキストと長めの説明を
12.2 サンプルの作り方
- cURL・JavaScript(fetch/axios)・PHP(HTTPクライアント)・Python(requests)の最低4言語
- コピーボタンはキーボード操作で押せる
- 長いコードは折りたたみ可能。見出しと説明を必ず付ける
- 失敗例(401/403/422/429/500)も掲載し、どう直せば良いかを短文で
12.3 エラーカタログ
type ごとに固定URLで公開。原因・対処・再現条件・リトライ可否・サポート窓口を明記。色は補助に留め、テキストが主役。
13. 実装サンプル:注文API(抜粋)
13.1 ルート
Route::prefix('api/v1')->middleware(['auth:sanctum', 'throttle:120,1'])->group(function () {
Route::get('/orders', [OrderController::class,'index']);
Route::post('/orders', [OrderController::class,'store'])->middleware('idem.key'); // 冪等性
Route::get('/orders/{order}', [OrderController::class,'show']);
Route::post('/orders/{order}/cancel', [OrderController::class,'cancel']);
});
13.2 検証
class StoreOrderRequest extends FormRequest {
public function rules(): array {
return [
'items' => ['required','array','min:1'],
'items.*.sku' => ['required','string'],
'items.*.qty' => ['required','integer','min:1','max:100'],
'note' => ['nullable','string','max:500']
];
}
}
13.3 コントローラ(ETag対応の show)
public function show(Order $order)
{
$this->authorize('view', $order);
$etag = sha1($order->updated_at.$order->id);
if (request()->header('If-None-Match') === $etag) {
return response()->noContent(304)->header('ETag',$etag);
}
return response()->json([
'data' => [
'id'=>$order->id,
'status'=>$order->status,
'total'=>$order->total,
'items'=>$order->items()->get(['sku','name','qty','price']),
],
'meta' => [ 'currency'=>'JPY' ]
], 200)->header('ETag',$etag);
}
13.4 エラー(problem+json の422)
throw ValidationException::withMessages([
'items.0.sku' => ['このSKUは存在しません。']
]);
14. 契約テストと互換性チェック
14.1 コントラクトテスト
- OAS を読み込み、各エンドポイントについて実応答が仕様適合かをチェック
- スキーマ差分(backward incompatible)を CI で検出(例:型変更/必須化)
14.2 リグレッション抑止
- 代表的なクライアント(社内SDK)のE2Eスモークを毎デプロイで実施
trace_idをキーに、障害発生時の調査パスを短縮
15. モニタリングとSLO
- p50/p95/p99 のレイテンシ、エラー率(5xx/4xx)、レート制限到達率、キャッシュヒット率
- バックエンドのキュー遅延、データベースのスロークエリ
- 重要エンドポイントの SLO(例:p95<300ms、5xx<0.1%)を明文化し、逸脱時にアラート
16. セキュリティ強化の要点
- HSTS・CSP・
X-Content-Type-Options: nosniff・Referrer-Policy - 入力検証(
FormRequest)と、SQL/テンプレート/コマンドインジェクション対策 - ファイル受け取り時の MIME 検証・サイズ制限・ウイルススキャン
- 監査ログ:
trace_id・user_id・ip・path・status・latencyを構造化して保存 - 機微情報のマスキング(トークン/カード/個人識別子)
17. アクセシブルなサンプルアプリとポータル
- ポータルのナビゲーションはキーボードで操作でき、フォーカスリングが見える
- コードブロックに言語ラベルとコピーボタン、色弱でも区別可能な配色
- 表やパラメータ定義には見出しと説明を用意。アイコンだけで説明しない
- サンプルリクエストは curl と各言語で同じ内容を示し、レスポンスも整形して両方提供
- ダークモード対応と十分なコントラスト比(WCAG AA 以上)
18. よくある落とし穴と回避策
- 任意ソート/フィルタをそのままSQLへ → 列挙と妥当性検証でガード
- 形だけのバージョン番号 → 破壊的変更を避ける運用と移行ガイドをセットで
- 大きな一覧の全件返却 → ページング必須、ETag/条件付きで再取得コストを削減
- 不統一なエラーフォーマット → RFC 7807 で統一し、エラーカタログへ誘導
- 冪等性なしの作成系 →
Idempotency-Keyとサーバ側のロック/キャッシュ - Webhook のなりすまし → 署名検証とリプレイ対策、イベントIDの重複無視
- ドキュメントの色依存 → テキスト主導、代替テキスト、キーボードで完遂できるUI
- 追跡不能な障害 →
trace_idを全レスポンスに付与し、ログと紐付け
19. チェックリスト(配布用)
設計
- [ ] スタイル(REST/JSON:API/GraphQL)を選定し、逸脱ルールを定義
- [ ] OpenAPI を単一の真実に。契約テストをCIに組込み
- [ ] バージョニングは
/api/v{n}、破壊的変更の方針を明文化
機能
- [ ] 認証(Sanctum/OAuth)とスコープで最小権限
- [ ] ページング/ソート/フィルタは列挙と上限値
- [ ] ETag/条件付きリクエストで帯域削減
- [ ] レート制限・idempotency・リトライ方針
エラー
- [ ] RFC 7807 で統一、
trace_idを必ず返す - [ ] 多言語
detailとエラーカタログのURL
セキュリティ/運用
- [ ] 署名付き Webhook とリプレイ対策
- [ ] 監査ログの構造化とPIIマスク
- [ ] SLO とメトリクスのダッシュボード
ドキュメント/アクセシビリティ
- [ ] 目次・キーボード操作・コントラスト・代替テキスト
- [ ] cURL+複数言語のサンプルとコピー機能
- [ ] 失敗例と対処の明示
20. まとめ
API は、長く使われる公共物です。OpenAPI を中心に、変更の互換性を守り、エラー表現を統一し、キャッシュやレート制限で健全性を保ちましょう。作成系は idempotency で二重実行に強くし、Webhook は署名検証とリプレイ対策で堅牢に。ドキュメントは誰にでも読みやすく、試しやすく。色や画像だけに依存せず、テキストと構造で伝えます。今日の設計とサンプルを土台に、チームのAPI標準を整備してください。落ち着いて育てれば、利用者にも開発者にもやさしいプラットフォームになります。
参考リンク
- Laravel 公式
- 仕様・標準
- セキュリティ/運用
- ドキュメント/アクセシビリティ
