Open2

【Req/Resスキーマ定義】Result型の設計について📝

まさぴょん🐱まさぴょん🐱

Result型の設計についてo3に相談📝

Req/Resのスキーマ定義をする際に、
共通のResult型のようなものを作るとしたら、どんな構造がいいですか?

共通 Result 型を設計するときは 「人にも機械にも扱いやすいエンベロープ構造」 を 1 つ決めてしまうと、
API 全体のスキーマが揃い、型定義・ログ・リトライ判定がすべて楽になります。
以下では TypeScript を例に、最低限あるとうれしいフィールドを添えて示します。
(OpenAPI で書く場合も oneOf で全く同じ形を定義できます。)

1. ガイドライン

ポイント ねらい
トップレベルで成功/失敗を二分
ok: booleanstatus: "success" | "error"
クライアントは真偽値 1 つで分岐できる
HTTP ステータスは「通信状態」だけを示す 200 でも業務エラーを返したいときに混乱しない
成功時は data と任意の meta ページネーションや合計件数などを柔軟に渡せる
失敗時は error オブジェクト 1 つに集約 code(機械判定用)と message(人間向け)を必須に
全レスポンス共通で requestIdtimestamp ログ相関やリトライ時の冪等性確認が容易

2. 共通 Result 型(TypeScript/Zod でそのまま使える構造)

// --- 基本的なエラー表現 ---
export interface ApiError {
  /** 機械判定・翻訳用。例: "VALIDATION_ERROR" */
  code: string;
  /** エンドユーザーにそのまま見せられるメッセージ */
  message: string;
  /** 任意: バリデーション一覧やスタックトレースなど */
  details?: unknown;
}

// --- Result ジェネリック --- 
export type ApiResult<TData = unknown, TMeta = undefined> =
  | {
      ok: true;
      data: TData;
      /** 例: ページ情報や集計値 */
      meta: TMeta extends undefined ? never : TMeta;
      requestId: string;
      timestamp: string; // ISO-8601
    }
  | {
      ok: false;
      error: ApiError;
      requestId: string;
      timestamp: string;
    };

使い方例

// ユーザー詳細取得
type GetUserRes = ApiResult<{
  userId: string;
  name: string;
}>;

// 一覧取得(ページネーション付き)
type ListUsersRes = ApiResult<
  { userId: string; name: string }[],
  { page: number; perPage: number; total: number }
>;

3. 実際の JSON 例

成功

{
  "ok": true,
  "data": {
    "userId": "123e4567-e89b-12d3-a456-426614174000",
    "name": "Alice"
  },
  "requestId": "req_05HY...Tx",
  "timestamp": "2025-05-12T18:45:02.123Z"
}

失敗

{
  "ok": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "メールアドレスは必須です",
    "details": [{ "field": "email", "reason": "required" }]
  },
  "requestId": "req_05HY...Tx",
  "timestamp": "2025-05-12T18:45:02.456Z"
}

4. なぜこの形が扱いやすいか

  1. 型安全
    if (res.ok) { /* data にだけアクセス可 */ } というガードで TypeScript が自動絞り込み。

  2. バックエンドの言語を問わない
    Rust の Result<T, E> や Go の error パターンとも相性が良く、
    シリアライズ時にこの JSON へ簡単に写像できる。

  3. ログ&モニタリングが一貫
    requestIdtimestamp が必ず来るので ELK/Sentry で束ねやすい。

  4. フロントのエラー表示・国際化が楽
    error.code を i18n キーとしてそのまま利用でき、メッセージはフォールバックに。

5. 小技・拡張

シーン 追加フィールド例
冪等性保証 idempotencyKey 同じキーで複数回実行 → 最初の requestId を返す
リソース生成 location 新規作成時に GET /resource/:id の URL を通知
リトライ猶予 retryAfter 一時的失敗時 (429 / 503) に秒数を示す

まとめ

  • トップレベルで ok を真偽値にし、data / error を排他にする
  • requestIdtimestamp は全レスポンス共通
  • エラーは code, message, details に集約

この 3 点を押さえると バックエンド実装・OpenAPI 定義・フロントの型生成 すべてがスムーズになります。