REST APIエラーハンドリング設計ガイド ―― 一貫性のある使いやすいAPIのために

APIの開発において、適切なエラーハンドリングの設計は非常に重要な要素です。エラーメッセージが不明確だったり、HTTPステータスコードの使い方が不適切だったりすると、APIを利用する開発者の経験を大きく損ない、デバッグ時間の増加やサポートチケットの増加につながってしまいます。本記事では、開発者体験を向上させるための実践的なAPIエラーハンドリング設計について解説します。

下記を参考にしました

https://zuplo.com/blog/2025/02/11/best-practices-for-api-error-handling

## 想定読者

- RESTful APIを設計・開発している方
- APIのエラーハンドリングを改善したい方
- WebアプリケーションのバックエンドAPIを担当している方

## 前提知識

- HTTP/RESTの基本的な知識
- JSONでのレスポンス形式の理解
- 基本的なWebセキュリティの知識

## APIエラーハンドリングの基本原則

APIのエラーハンドリングには、以下の3つの重要な要素があります：

1. **適切なHTTPステータスコードの使用**：エラーの種類を正確に表現する
2. **構造化されたエラーレスポンス**：一貫性のある明確なフォーマット
3. **セキュアなエラーメッセージ**：必要な情報のみを提供し、脆弱性を作らない

それでは、各要素について詳しく見ていきましょう。

## HTTPステータスコードの適切な使用

HTTPステータスコードは、エラーの種類を伝える最初の手段です。ステータスコードは適切に使い分けることが重要です。以下に主要なステータスコードの使用例を示します：

### クライアントエラー (4xx)

```
400 Bad Request        - リクエストの形式が不正
401 Unauthorized      - 認証が必要
403 Forbidden        - アクセス権限がない
404 Not Found        - リソースが存在しない
422 Unprocessable Entity - リクエストの形式は正しいが、内容が不正
429 Too Many Requests   - レート制限超過
```

### サーバーエラー (5xx)

```
500 Internal Server Error - サーバー内部のエラー
503 Service Unavailable  - サービスが一時的に利用不可
504 Gateway Timeout     - 外部サービスとの通信がタイムアウト
```

## 構造化されたエラーレスポース

エラーレスポンスは、RFC 9457（Problem Details）仕様に従うことで、一貫性のある分かりやすい形式を実現できます。

```json
{
  "type": "https://api.example.com/errors/invalid-input",
  "title": "入力パラメータが不正です",
  "detail": "メールアドレスは user@domain.com 形式で指定してください"
}
```

Problem Details形式では、カスタムメンバーを追加して拡張することもできます。クライアントｎは認識できない拡張メンバーが含まれている場合には無視するようなスキーマチェックが必要かもしれません：

```json
{
  "type": "https://api.example.com/errors/invalid-input",
  "title": "入力パラメータが不正です",
  "status": 422,
  "detail": "メールアドレスは user@domain.com 形式で指定してください",
  "instance": "/transactions/auth/2024-02-10/125",
  "traceId": "a1b2c3d4-e5f6-7890",  // 拡張メンバー
  "errors": {                        // 拡張メンバー
    "email": ["Invalid format"]
  }
}
```

各フィールドの意味は以下の通りです：

必須フィールド：
- **type**: エラーの種類を示すURI
- **title**: エラーの簡潔な説明

オプショナルフィールド：
- **status**: HTTPステータスコード
- **detail**: エラーの詳細な説明
- **instance**: エラーが発生した特定のリソースを示すURI

このレスポンスには以下のヘッダーを付与します：

```http
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
Content-Language: ja
```

## 実装例

では、実際の実装例を見ていきましょう。ここではNode.js/Expressでの実装例を示します：

```javascript
// カスタムエラークラスの定義
class APIError extends Error {
  constructor(status, title, detail, type) {
    super(detail);
    this.status = status;
    this.title = title;
    this.detail = detail;
    this.type = type || 'https://api.example.com/errors/general-error';
  }
}

// エラーハンドリングミドルウェア
app.use((err, req, res, next) => {
  if (err instanceof APIError) {
    // APIエラーの場合
    res.status(err.status).json({
      type: err.type,
      title: err.title,
      status: err.status,
      detail: err.detail,
      instance: req.originalUrl
    });
  } else {
    // 予期せぬエラーの場合
    console.error(err);
    res.status(500).json({
      type: 'https://api.example.com/errors/internal-error',
      title: 'Internal Server Error',
      status: 500,
      detail: '予期せぬエラーが発生しました',
      instance: req.originalUrl
    });
  }
});

// 使用例
app.post('/users', async (req, res, next) => {
  try {
    const { email } = req.body;
    
    if (!email || !email.includes('@')) {
      throw new APIError(
        422,
        '入力パラメータが不正です',
        'メールアドレスは user@domain.com 形式で指定してください',
        'https://api.example.com/errors/invalid-email'
      );
    }

    // ユーザー作成処理...
    
  } catch (err) {
    next(err);
  }
});
```

## セキュリティに関する考慮事項

エラーメッセージの設計では、セキュリティにも十分な注意を払う必要があります。不適切なエラーメッセージは、攻撃者に有用な情報を提供してしまう可能性があります。例えば：

- データベースのエラーメッセージから、使用しているDBMSの種類やバージョンが特定される
- スタックトレースから、使用しているライブラリやそのバージョンが判明する
- 内部システムの構成やホスト名が露出する
- 個人情報やセンシティブな業務データが漏洩する

これらの情報は、攻撃の足がかりとして使用される可能性があります。以下のポイントを意識して、適切なエラーメッセージの設計を心がけましょう：

1. **エラーメッセージのSanitization**
   エラーメッセージは適切にsanitizeする必要があります：

   - 内部システムの詳細を含めない
   ```javascript
   // 危険な例 - 内部情報の漏洩
   throw new APIError(500, 'データベースエラー', 
     'Error executing query: SELECT * FROM users WHERE email = "test@example.com" - Connection refused to db-prod-01.internal:5432');

   // 適切な例
   throw new APIError(500, '内部エラー',
     'サービスが一時的に利用できません。しばらく経ってから再度お試しください。');
   ```

   - スタックトレースを本番環境で表示しない
   ```javascript
   // 危険な例 - スタックトレースの漏洩
   app.use((err, req, res, next) => {
     res.status(500).json({
       message: err.message,
       stack: err.stack  // 本番環境で公開すべきでない
     });
   });

   // 適切な例
   app.use((err, req, res, next) => {
     const response = {
       type: 'https://api.example.com/errors/internal-error',
       title: '内部エラー'
     };
     
     if (process.env.NODE_ENV === 'development') {
       response.debug = {
         message: err.message,
         stack: err.stack
       };
     }
     
     res.status(500).json(response);
   });
   ```

   - データベースのエラーメッセージをそのまま返さない
   ```javascript
   // 危険な例 - SQLエラーの漏洩
   catch (err) {
     throw new APIError(400, 'Database Error', err.message);
     // 例: "Duplicate entry 'test@example.com' for key 'users.email_unique'"
   }

   // 適切な例
   catch (err) {
     if (err.code === 'ER_DUP_ENTRY') {
       throw new APIError(400, '入力エラー', 
         'このメールアドレスは既に登録されています');
     }
     throw new APIError(500, '内部エラー',
       'データの処理中にエラーが発生しました');
   }
   ```

   - 個人情報（PII）を含めない
   ```javascript
   // 危険な例 - 個人情報の漏洩
   throw new APIError(404, 'ユーザーが見つかりません',
     `User ${email} (ID: ${userId}, location: ${userLocation}) not found`);

   // 適切な例
   throw new APIError(404, 'ユーザーが見つかりません',
     'アカウントが存在しないか、アクセス権限がありません');
   ```

2. **認証関連のエラー**
   - ユーザーの存在有無を推測できる情報を含めない
   - 一般的なメッセージを使用する

```javascript
// 悪い例
if (!user) {
  throw new APIError(401, 'エラー', 'ユーザー test@example.com は存在しません');
}

// 良い例
if (!user) {
  throw new APIError(401, '認証エラー', 'メールアドレスまたはパスワードが正しくありません');
}
```

3. **エラーメッセージの標準化**
   - 一貫性のあるメッセージ形式を使用
   - エラーコードを体系的に管理

## エラーハンドリングのテストと監視

適切なエラーハンドリングを維持するためには、テストと監視が欠かせません：

1. **エラーケースのテスト**
```javascript
describe('User API Error Handling', () => {
  it('should return 422 for invalid email format', async () => {
    const response = await request(app)
      .post('/users')
      .send({ email: 'invalid-email' });
    
    expect(response.status).toBe(422);
    expect(response.body).toHaveProperty('type');
    expect(response.body).toHaveProperty('detail');
  });
});
```

2. **エラー発生率の監視**
- エラーの種類と頻度の追跡
- 重要なエラーの即時通知
- エラー解決時間の測定

## まとめ

効果的なAPIエラーハンドリングは、以下の要素を組み合わせることで実現できます：

- HTTPステータスコードの適切な使用
- 構造化されたエラーレスポンスフォーマット
- セキュリティを考慮したメッセージング
- 包括的なテストと監視

これらの原則に従うことで、開発者にとって使いやすく、運用・保守がしやすいAPIを実現できます。

## References

Main Article:
- [Best Practices for Consistent API Error Handling](https://zuplo.com/blog/2025/02/11/best-practices-for-api-error-handling) - Zuplo Blog

Additional Resources:
- [Best Practices for API Error Handling](https://blog.postman.com/best-practices-for-api-error-handling/) - Postman Blog
- [RFC 9457 - Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc9457.html)
- [Google API Design Guide - Error Handling](https://google.aip.dev/193)
- [MDN Web Docs - HTTP Response Status Codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status)