© TOOLS BOX — Next.js / React / TypeScript コードサンプル集

サンプルガイド
←サンプル一覧
nextjsapi

Next.js Route Handler でエラーレスポンスを型安全に統一する

Route Handler のエラーレスポンスを型付きで統一し、400 / 404 / 500 を一貫して扱うラッパー関数を実装する例。try/catch パターンとエラー型の整理も示す。

難易度: 中級·更新: 2026-04-18

対応バージョン

nextjs 15react 19

前提環境

Next.js Route Handler の基本と TypeScript の型定義を理解していること

概要

Route Handler 内のエラー処理をラッパー関数と型付きカスタムエラーで統一する。ハンドラごとに try/catch を書き直す代わりに、共通の withErrorHandler でラップし、400 / 404 / 500 を一貫したレスポンス形式で返す。

インストール

# 追加インストールは不要

実装

エラー型とレスポンス型の定義

// lib/api-error.ts
export type ErrorCode = "BAD_REQUEST" | "NOT_FOUND" | "INTERNAL_ERROR";

export type ErrorResponse = {
  error: {
    code: ErrorCode;
    message: string;
  };
};

export class ApiError extends Error {
  constructor(
    public readonly status: number,
    public readonly code: ErrorCode,
    message: string
  ) {
    super(message);
    this.name = "ApiError";
  }

  static badRequest(message: string) {
    return new ApiError(400, "BAD_REQUEST", message);
  }

  static notFound(message: string) {
    return new ApiError(404, "NOT_FOUND", message);
  }

  static internal(message = "Internal server error") {
    return new ApiError(500, "INTERNAL_ERROR", message);
  }
}

エラーハンドララッパー

// lib/with-error-handler.ts
import { NextResponse } from "next/server";
import { ApiError, ErrorResponse } from "./api-error";

type RouteHandler = (request: Request, context: unknown) => Promise<Response>;

export function withErrorHandler(handler: RouteHandler): RouteHandler {
  return async (request, context) => {
    try {
      return await handler(request, context);
    } catch (err) {
      if (err instanceof ApiError) {
        return NextResponse.json<ErrorResponse>(
          { error: { code: err.code, message: err.message } },
          { status: err.status }
        );
      }
      console.error(err);
      return NextResponse.json<ErrorResponse>(
        { error: { code: "INTERNAL_ERROR", message: "Internal server error" } },
        { status: 500 }
      );
    }
  };
}

Route Handler での使用例

// app/api/users/[id]/route.ts
import { NextResponse } from "next/server";
import { ApiError } from "@/lib/api-error";
import { withErrorHandler } from "@/lib/with-error-handler";

const users: Record<string, { id: string; name: string }> = {
  "1": { id: "1", name: "山田太郎" },
};

export const GET = withErrorHandler(async (_request, context) => {
  const { id } = (context as { params: { id: string } }).params;

  if (!id || typeof id !== "string") {
    throw ApiError.badRequest("id is required");
  }

  const user = users[id];
  if (!user) {
    throw ApiError.notFound(`User ${id} not found`);
  }

  return NextResponse.json(user);
});

入力バリデーションを含む POST の例

// app/api/users/route.ts
import { NextResponse } from "next/server";
import { ApiError } from "@/lib/api-error";
import { withErrorHandler } from "@/lib/with-error-handler";

type CreateUserInput = { name: string; email: string };

function validateCreateUser(body: unknown): CreateUserInput {
  if (
    typeof body !== "object" ||
    body === null ||
    typeof (body as Record<string, unknown>).name !== "string" ||
    typeof (body as Record<string, unknown>).email !== "string"
  ) {
    throw ApiError.badRequest("name and email are required");
  }
  return body as CreateUserInput;
}

export const POST = withErrorHandler(async (request) => {
  let body: unknown;
  try {
    body = await request.json();
  } catch {
    throw ApiError.badRequest("Invalid JSON body");
  }

  const input = validateCreateUser(body);

  // 実際の保存処理(省略)
  const created = { id: crypto.randomUUID(), ...input };

  return NextResponse.json(created, { status: 201 });
});

レスポンス例

// 400 Bad Request
{
  "error": {
    "code": "BAD_REQUEST",
    "message": "name and email are required"
  }
}

// 404 Not Found
{
  "error": {
    "code": "NOT_FOUND",
    "message": "User 999 not found"
  }
}

// 500 Internal Server Error
{
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "Internal server error"
  }
}

ポイント

  • ApiError にステータスコードと ErrorCode を持たせることで、ハンドラ内では throw ApiError.notFound(...) とだけ書けば済む
  • withErrorHandler が ApiError 以外の予期しないエラーも 500 に変換するため、未ハンドルエラーが素のスタックトレースでクライアントに漏れるのを防ぐ
  • ErrorResponse 型を共通定義しておくと、クライアント側でもレスポンスの型を import して使えて型安全になる
  • nextjs-route-handler-crud との使い分け: ハッピーパスの CRUD は nextjs-route-handler-crud、エラーレスポンス設計を整備したいときにこのパターンを組み合わせる
  • Zod でバリデーションする場合は zod-api-response-validation と組み合わせると ZodError を ApiError.badRequest に変換できる

注意点

nextjs-route-handler-crud はハッピーパスの CRUD。これはエラーパターンの設計(型付きエラーレスポンス・エラーラッパー・HTTP ステータス対応)に特化している。

関連サンプル

同じテーマや技術スタックを使った実装例

  • Jest で Next.js Route Handler をテストする

    Request / Response オブジェクトを直接渡して Route Handler 関数をユニットテストする例。GET / POST それぞれのハッピーパスとエラーパスを検証する。

  • Next.js Route Handler でサーバーサイドキーワード検索を実装する

    クエリパラメータで受け取ったキーワードを Route Handler でサーバーサイドフィルタし、ページネーション付きで返す例。クライアントのデバウンス入力と合わせた構成も示す。

  • Next.js Route Handler でクエリパラメータを使ってソートを実装する

    sort キーと order(asc/desc)をクエリパラメータで受け取り、Route Handler でデータをソートして返すパターンと、クライアント側のソートボタン UI を示す例。

  • Next.js Route Handler でファイルアップロードを受信・保存する

    Route Handler で multipart/form-data を受信し、アップロードされたファイルをサーバー側に保存する例。ファイルサイズ・拡張子バリデーションパターンも示す。

  • Next.js の global-error.tsx でルートレベルのエラーをキャッチする

    app/global-error.tsx を使い、ルートレイアウトを含む最上位のランタイムエラーをキャッチして reset ボタンで回復する実装例。error.tsx との責務の違いも示す。

関連仕様

このサンプルを理解するのに役立つ仕様や概念

  • FrameworkNext.jsReact ベースのフルスタックフレームワーク。SSR・SSG・App Router・API Routes を提供する。
←サンプル一覧に戻る