LESSON 15分

ストーリー

高橋アーキテクト
API設計で最も見落とされがちなのが、エラーハンドリングだ

高橋アーキテクトが画面を指差した。そこには 500 Internal Server Error と表示されている。

高橋アーキテクト
このエラーメッセージから、何が問題か分かるか?
あなた
…分かりません。サーバーで何かが起きた、としか
高橋アーキテクト
そうだ。これではクライアント開発者は途方に暮れる。エラーの設計が甘いと、障害対応に何倍もの時間がかかる。“何が問題で、どうすれば解決できるか”を伝えるのが良いエラー設計だ

エラーレスポンスの設計

RFC 7807: Problem Details

// RFC 7807(Problem Details for HTTP APIs)に基づく標準形式
interface ProblemDetails {
  type: string;       // エラーの種類を識別するURI
  title: string;      // 人間が読めるエラータイトル
  status: number;     // HTTPステータスコード
  detail: string;     // エラーの詳細説明
  instance: string;   // このエラーが発生した具体的なURI
}

// 実装例
const errorResponse = {
  type: "https://api.example.com/errors/validation",
  title: "Validation Error",
  status: 422,
  detail: "リクエストの入力値に問題があります",
  instance: "/api/v1/users",
  // 拡張フィールド(RFC 7807で許容されている)
  errors: [
    {
      field: "email",
      message: "メールアドレスの形式が正しくありません",
      value: "not-an-email"
    },
    {
      field: "age",
      message: "年齢は0以上の整数を指定してください",
      value: -5
    }
  ],
  traceId: "req_abc123def456"
};

エラーレスポンスの構造

// 統一されたエラーレスポンス型
interface ApiErrorResponse {
  error: {
    code: string;          // 機械可読なエラーコード
    message: string;       // ユーザー向けメッセージ
    details?: ErrorDetail[];  // 詳細なエラー情報
    traceId: string;       // トレース用ID
  };
}

interface ErrorDetail {
  field?: string;          // エラーが発生したフィールド
  message: string;         // 詳細メッセージ
  value?: unknown;         // 送信された値
}

// 使用例
// 400 Bad Request
{
  "error": {
    "code": "INVALID_REQUEST",
    "message": "リクエストの形式が正しくありません",
    "traceId": "req_abc123"
  }
}

// 422 Unprocessable Entity
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "入力内容に問題があります",
    "details": [
      { "field": "email", "message": "必須項目です" },
      { "field": "password", "message": "8文字以上必要です", "value": "***" }
    ],
    "traceId": "req_def456"
  }
}

エラーカテゴリとステータスコードの対応

// エラーコードの体系化
const ERROR_CODES = {
  // 認証・認可エラー (401, 403)
  UNAUTHENTICATED: { status: 401, message: '認証が必要です' },
  TOKEN_EXPIRED: { status: 401, message: 'トークンが期限切れです' },
  FORBIDDEN: { status: 403, message: 'この操作を行う権限がありません' },

  // バリデーションエラー (400, 422)
  INVALID_REQUEST: { status: 400, message: 'リクエストの形式が不正です' },
  VALIDATION_ERROR: { status: 422, message: '入力内容に問題があります' },

  // リソースエラー (404, 409)
  NOT_FOUND: { status: 404, message: 'リソースが見つかりません' },
  ALREADY_EXISTS: { status: 409, message: '既に存在するリソースです' },

  // レート制限 (429)
  RATE_LIMITED: { status: 429, message: 'リクエスト数が制限を超えました' },

  // サーバーエラー (500, 503)
  INTERNAL_ERROR: { status: 500, message: 'サーバー内部エラーが発生しました' },
  SERVICE_UNAVAILABLE: { status: 503, message: 'サービスは一時的に利用できません' },
} as const;

セキュリティとエラーメッセージ

情報漏洩を防ぐ

// 悪い例: 内部情報を漏らす
{
  "error": {
    "message": "PostgreSQL error: relation \"users\" does not exist",
    "stack": "Error at UserRepository.findById (/app/src/repos/user.ts:42)"
  }
}

// 良い例: 必要最小限の情報
{
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "サーバー内部エラーが発生しました。問題が続く場合はサポートにお問い合わせください。",
    "traceId": "req_abc123"
  }
}
// 詳細なエラーはサーバーログに記録し、traceIdで追跡する

認証エラーの注意点

// 悪い例: ユーザーの存在を漏らす
POST /api/v1/login
// → "このメールアドレスは登録されていません"   // アカウントの存在が判明
// → "パスワードが間違っています"              // メールアドレスは正しいと判明

// 良い例: 曖昧なメッセージ
POST /api/v1/login
// → "メールアドレスまたはパスワードが正しくありません"

まとめ

ポイント内容
標準形式RFC 7807(Problem Details)を採用する
エラー構造code, message, details, traceId を含める
ステータスコードエラーの種類に応じて適切なコードを返す
セキュリティ内部情報を漏らさない、traceIdで追跡
一貫性全エンドポイントで同じエラー形式を使用

チェックリスト

  • RFC 7807 Problem Details の形式を理解した
  • エラーレスポンスの統一構造を設計できる
  • セキュリティを考慮したエラーメッセージの重要性を理解した
  • traceId によるエラー追跡の仕組みを把握した

次のステップへ

エラーハンドリングの設計を学びました。

次は Step 1 の理解度チェックです。 ここまで学んだREST APIの基本原則、ベストプラクティス、エラーハンドリングの知識を確認しましょう。


推定読了時間: 15分