Skip to content

レスポンスバリデーション

レスポンスバリデーションは、APIが定義されたスキーマに一致するデータを返すことを保証します。これにより、API利用者への型安全性を提供し、開発の早期段階でバグを発見できます。Koriのアーキテクチャは異なるバリデーションライブラリをサポートするよう設計されていますが、公式にはファーストクラスのZod統合を提供し、Standard Schemaもサポートしています。

このガイドではZodを例として使用します。

セットアップ

Zod統合パッケージをインストール:

bash
npm install @korix/zod-schema-adapter zod

レスポンスバリデーション付きのKoriアプリケーションをセットアップ:

typescript
import { createKori } from '@korix/kori';
import {
  zodResponseSchema,
  enableZodResponseValidation,
} from '@korix/zod-schema-adapter';
import { z } from 'zod';

const app = createKori({
  ...enableZodResponseValidation(),
});

基本例

異なるステータスコードに対するレスポンススキーマを定義:

typescript
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  age: z.number().int().min(0),
  createdAt: z.string(),
});

const ErrorSchema = z.object({
  error: z.object({
    type: z.string(),
    message: z.string(),
  }),
});

app.get('/users/:id', {
  responseSchema: zodResponseSchema({
    '200': UserSchema,
    '404': ErrorSchema,
    '500': ErrorSchema,
  }),
  handler: (ctx) => {
    const { id } = ctx.req.pathParams();
    const userId = Number(id);

    if (userId === 999) {
      // この404レスポンスはErrorSchemaに対してバリデーションされる
      return ctx.res.notFound({ message: 'User not found' });
    }

    // この200レスポンスはUserSchemaに対してバリデーションされる
    return ctx.res.status(200).json({
      id: userId,
      name: 'John Doe',
      age: 30,
      createdAt: new Date().toISOString(),
    });
  },
});

注意:レスポンスバリデーションは、ランタイムでデータのみをチェックします。ctx.res.json()とスキーマの不一致はTypeScriptでは検出されません。これらはハンドラー完了後に検出されます。

レスポンススキーマパターン

ステータスコードマッチング

レスポンススキーマは複数のステータスコードパターンをサポートします:

typescript
app.post('/users', {
  responseSchema: zodResponseSchema({
    // 正確なステータスコード
    '201': UserSchema,
    '400': ErrorSchema,
    '409': ErrorSchema,

    // ワイルドカードパターン(5で始まるステータスコードにマッチ)
    '5XX': ErrorSchema,

    // 指定されていないステータスコードのデフォルトフォールバック
    default: ErrorSchema,
  }),
  handler: (ctx) => {
    // ハンドラーロジック
  },
});

コンテンツタイプサポート

異なるコンテンツタイプに対して異なるスキーマを定義:

typescript
const HtmlErrorSchema = z.string();
const JsonErrorSchema = z.object({
  error: z.string(),
  details: z.array(z.string()).optional(),
});

app.get('/data', {
  responseSchema: zodResponseSchema({
    '200': UserSchema,
    '400': {
      content: {
        'application/json': JsonErrorSchema,
        'text/html': HtmlErrorSchema,
      },
    },
  }),
  handler: (ctx) => {
    // ハンドラーロジック
  },
});

エラーハンドリング

レスポンスバリデーションエラーは、リクエストバリデーションエラーとは異なって処理されます。デフォルトでは、バリデーション失敗はログに記録されますが、クライアントに送信されるレスポンスには影響しません。

デフォルト動作

レスポンスバリデーションが失敗した場合:

  • アプリケーションログに警告が記録される
  • 元のレスポンスがクライアントに変更なしで送信される
  • クライアントにエラーは投げられない

これにより、レスポンスバリデーションの問題がエンドユーザーにとってAPIを壊すことがないよう保証されます。

カスタムエラーハンドラー

カスタムレスポンスバリデーションエラーハンドラーを提供できます:

ルートレベルエラーハンドラー

typescript
app.get('/users/:id', {
  responseSchema: zodResponseSchema({
    '200': UserSchema,
  }),
  onResponseValidationFailure: (ctx, error) => {
    // より多くのコンテキストでバリデーションエラーをログ
    ctx.log().error('Response validation failed', {
      path: ctx.req.url().pathname,
      status: ctx.res.getStatus(),
      error,
    });

    // 任意で異なるレスポンスを返却
    return ctx.res.internalError({
      message: 'Invalid response format',
    });
  },
  handler: (ctx) => {
    // ハンドラーロジック
  },
});

インスタンスレベルエラーハンドラー

typescript
const app = createKori({
  ...enableZodResponseValidation(),
  onResponseValidationFailure: (ctx, error) => {
    // グローバルレスポンスバリデーションエラーハンドリング
    ctx.log().error('Response validation failed globally', { error });

    // 元のレスポンスを使用するためにundefinedを返却
  },
});

ハンドラーの優先順位

レスポンスバリデーションエラーハンドラーは、リクエストバリデーションと同じ優先順位に従います:

  1. ルートレベルハンドラー(提供されている場合)
  2. インスタンスレベルハンドラー(提供されている場合)
  3. デフォルト動作(警告をログ、元のレスポンスを送信)

各ハンドラーは、レスポンスを返さずに次のハンドラーに渡すことでエラーを処理するか渡すかを選択できます。これにより、特定のハンドラーが特定のエラータイプのみを処理することが可能になります。

ストリームレスポンスの処理

レスポンスバリデーションは、クライアントに送信される前にバリデーションできないため、ストリーミングレスポンスのバリデーションを自動的にスキップします。

typescript
app.get('/download', {
  responseSchema: zodResponseSchema({
    '200': z.string(), // これはストリームに対してバリデーションされない
  }),
  handler: (ctx) => {
    // ストリーミングレスポンスはバリデーションされない
    return ctx.res.stream(someReadableStream);
  },
});

Released under the MIT License.