← 전체 글로 돌아가기

Next.js

Next.js Route Handler에서 JSON 에러 응답 통일하기

API 에러 응답 형식이 제각각이면 클라이언트에서 처리하기 힘들다. 한 가지 패턴으로 통일하자.

Next.js Route Handler를 여러 개 만들다 보면 에러 응답이 제각각이 되기 쉽다. 어떤 건 { error: "..." }, 어떤 건 { message: "..." }, 어떤 건 HTTP 상태만 반환한다.

에러 응답 형식 정하기

모든 에러 응답을 같은 형식으로 통일하면 클라이언트에서 처리하기 쉬워진다.

type ErrorResponse = {
  success: false;
  error: {
    code: string;
    message: string;
    details?: unknown;
  };
};

type SuccessResponse<T> = {
  success: true;
  data: T;
};

에러 응답 헬퍼 함수 만들기

Route Handler에서 반복되는 코드를 피하려면 헬퍼를 만든다.

// lib/api-response.ts
export function errorResponse(
  code: string,
  message: string,
  status: number = 400,
  details?: unknown
) {
  return Response.json(
    {
      success: false,
      error: { code, message, details },
    },
    { status }
  );
}

export function successResponse<T>(data: T, status: number = 200) {
  return Response.json(
    { success: true, data },
    { status }
  );
}

Route Handler에서 사용하기

// app/api/items/[id]/route.ts
import { successResponse, errorResponse } from '@/lib/api-response';

export async function GET(request: Request, { params }: Props) {
  const id = params.id;

  if (!id || typeof id !== 'string') {
    return errorResponse(
      'INVALID_ID',
      'Item ID is required and must be a string',
      400
    );
  }

  try {
    const item = await db.items.findUnique({ where: { id } });
    if (!item) {
      return errorResponse(
        'NOT_FOUND',
        `Item with ID ${id} not found`,
        404
      );
    }
    return successResponse(item);
  } catch (error) {
    console.error('Error fetching item:', error);
    return errorResponse(
      'INTERNAL_ERROR',
      'Failed to fetch item',
      500,
      process.env.NODE_ENV === 'development' ? error : undefined
    );
  }
}

검증 에러도 통일하기

입력값 검증도 같은 형식으로 반환한다.

export async function POST(request: Request) {
  try {
    const body = await request.json();

    // 검증
    if (!body.name || typeof body.name !== 'string') {
      return errorResponse(
        'VALIDATION_ERROR',
        'Name is required and must be a string',
        400,
        { field: 'name' }
      );
    }

    if (!body.email || !isValidEmail(body.email)) {
      return errorResponse(
        'VALIDATION_ERROR',
        'Email must be valid',
        400,
        { field: 'email' }
      );
    }

    const item = await db.items.create({ data: body });
    return successResponse(item, 201);
  } catch (error) {
    return errorResponse('PARSE_ERROR', 'Invalid JSON', 400);
  }
}

클라이언트에서 처리하기

에러 형식이 통일되면 클라이언트 코드도 간단해진다.

const response = await fetch(`/api/items/${id}`);
const json = await response.json();

if (!json.success) {
  // 모든 에러가 같은 구조
  console.error(`Error: ${json.error.code} - ${json.error.message}`);

  if (json.error.code === 'NOT_FOUND') {
    showNotFoundPage();
  } else if (json.error.code === 'VALIDATION_ERROR') {
    showValidationErrors(json.error.details);
  } else {
    showGenericError(json.error.message);
  }
} else {
  // 성공 시에도 일관된 형식
  processItem(json.data);
}

API 응답 형식을 처음부터 통일하면 나중에 추가하는 Route Handler도 일관성 있게 작성할 수 있다.