PHPで学ぶAPIのベストプラクティス
目次
学習の経緯
個人開発でフロントエンドとバックエンドを分離した構成を試みたとき、「API設計って何から考えればいいんだろう」と迷った経験があります。最初はなんとなくエンドポイントを作っていましたが、規模が大きくなるにつれて命名が揺れたり、エラーの返し方がバラバラになったりと、メンテナンスが辛くなっていきました。この記事では、PHPでAPIを実装する際に意識すべきベストプラクティスをまとめます。
RESTful APIとは何か
REST(Representational State Transfer)は、HTTPプロトコルの特性(メソッド、URL、ステータスコード)を活かしてリソースを操作する設計思想です。ポイントは「URLに動詞を入れない」こと。
GET /users → ユーザー一覧を取得
POST /users → ユーザーを作成
GET /users/1 → ID=1のユーザーを取得
PUT /users/1 → ID=1のユーザーを更新
DELETE /users/1 → ID=1のユーザーを削除
Amazon Web Services, Inc.
RESTful API とは? – RESTful API の説明 – AWS
RESTful API とは何か、企業が RESTful API を使用する方法と理由、AWS で API Gateway を使用する方法をご覧ください。
エンドポイント設計のベストプラクティス
- リソース名は複数形の名詞で統一する(
/users,/products) - 階層関係は
/で表現する(/users/1/orders) - バージョニングはURLに含める(
/api/v1/users)
// ルーティング例(Slimフレームワーク)
$app->get('/api/v1/users', [UserController::class, 'index']);
$app->post('/api/v1/users', [UserController::class, 'store']);
$app->put('/api/v1/users/{id}', [UserController::class, 'update']);
$app->delete('/api/v1/users/{id}', [UserController::class, 'destroy']);
レスポンス形式とステータスコード
サーバーからクライアントへのレスポンスは、以下の表のようなステータスコードがあります。
これらを使い、適切にサーバーから返すことでクライアント側から見て分かりやすいレスポンスとなります。
| コード | 意味 | 使いどころ |
|---|---|---|
| 200 | OK | 取得・更新成功 |
| 201 | Created | 作成成功 |
| 400 | Bad Request | バリデーションエラー |
| 401 | Unauthorized | 未認証 |
| 403 | Forbidden | 権限なし |
| 404 | Not Found | リソース未存在 |
| 500 | Internal Server Error | サーバーエラー |
<?php
function jsonResponse(array $data, int $status = 200): void {
http_response_code($status);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
exit;
}
// エラーレスポンス例
jsonResponse(['status' => 'error', 'message' => 'ユーザーが見つかりません', 'code' => 'USER_NOT_FOUND'], 404);
認証:Bearer Token / JWT
JWTは「ヘッダー.ペイロード.署名」の3パーツをBase64URLエンコードしたトークンで、サーバー側にセッションを持たずに認証できます。
<?php
function authenticateRequest(): array {
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (!str_starts_with($authHeader, 'Bearer ')) {
jsonResponse(['status' => 'error', 'message' => '認証トークンがありません'], 401);
}
$token = substr($authHeader, 7);
$decoded = JWT::decode($token, new Key(SECRET_KEY, 'HS256'));
return (array) $decoded;
}
APIキー認証の場合は hash_equals() を使ってタイミング攻撃を防ぐことが重要です。
バリデーションとエラーハンドリング
ログイン画面やフォーム画面など、ユーザーの入力に対する判定をバリデーションといいます。
<?php
function validateUserInput(array $input): array {
$errors = [];
if (empty($input['name'])) {
$errors['name'] = '名前は必須です';
} elseif (mb_strlen($input['name']) > 50) {
$errors['name'] = '名前は50文字以内で入力してください';
}
if (!filter_var($input['email'], FILTER_VALIDATE_EMAIL)) {
$errors['email'] = 'メールアドレスの形式が正しくありません';
}
return $errors;
}
レート制限の考え方
Redisを使ったシンプルなレート制限:
<?php
function checkRateLimit(string $identifier, int $limit = 60, int $window = 60): void {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$key = "rate_limit:{$identifier}";
$current = $redis->incr($key);
if ($current === 1) { $redis->expire($key, $window); }
if ($current > $limit) {
jsonResponse(['status' => 'error', 'message' => 'リクエスト制限を超えました'], 429);
}
}
まとめ
- URLはリソース名(複数形名詞)で設計し、動詞は使わない
- HTTPメソッドとステータスコードを意味通りに使う
- 認証にはJWT or APIキーを使い、タイミング攻撃を防ぐ
- 入力値は必ずバリデーションしてからDBに渡す
- レート制限を設けて乱用を防ぐ
- エラーの詳細はログに残し、外部には漏らさない
