レビュー待ち·難易度: 中級·更新: 2026-04-18

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

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

nextjsapierror-handling

対応バージョン

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

注意点

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

関連サンプル