← 전체 글로 돌아가기

API

API 에러 응답 형식을 처음부터 제대로 잡는 법

에러 응답 구조를 나중에 바꾸려면 클라이언트 코드까지 다 뜯어야 한다. 초반에 형식을 잘 잡아두면 디버깅과 유지보수가 훨씬 편해진다.

API를 처음 만들 때 에러 응답을 대충 { message: 'not found' } 정도로 넘기다 보면, 나중에 클라이언트에서 에러 처리하는 코드가 제각각이 돼버린다. 형식을 한 번 정해두면 이후가 훨씬 편하다.

일관된 에러 응답 구조

내가 지금 쓰는 구조는 이렇다.

// 에러 응답 타입
interface ApiError {
  code: string;      // 기계가 읽는 에러 코드 (예: 'USER_NOT_FOUND')
  message: string;   // 사람이 읽는 설명
  details?: unknown; // 필드 검증 에러 등 추가 정보 (선택)
}

예시 응답:

{
  "code": "VALIDATION_ERROR",
  "message": "입력값이 올바르지 않습니다.",
  "details": {
    "email": "올바른 이메일 형식이 아닙니다."
  }
}

message만 있으면 클라이언트에서 에러 종류를 문자열 비교로 구분해야 하는데, code가 있으면 if (error.code === 'USER_NOT_FOUND') 형태로 명확하게 처리할 수 있다.

HTTP 상태 코드는 의미에 맞게

모든 에러를 200으로 내리고 body에 { success: false }를 넣는 패턴은 피한다. HTTP 상태 코드 자체가 의미를 가지고, 모니터링 도구들도 4xx/5xx를 기준으로 알림을 보낸다.

상황코드
리소스 없음404
인증 안 됨 (토큰 없음)401
권한 없음 (토큰은 있지만 접근 불가)403
입력값 오류400
서버 내부 오류500
요청 한도 초과429

스택 트레이스는 운영에서 노출하지 않는다

개발 환경에서는 스택 트레이스를 응답에 넣어두면 디버깅이 편하지만, 운영에서는 내부 파일 경로와 라이브러리 구조가 외부에 노출된다.

// Express 기준
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  const isDev = process.env.NODE_ENV !== 'production';
  res.status(500).json({
    code: 'INTERNAL_ERROR',
    message: isDev ? err.message : '서버 오류가 발생했습니다.',
    ...(isDev && { stack: err.stack }),
  });
});

에러 확인할 때

실제 응답이 설계한 대로 나오는지 확인할 때는 curl로 직접 때려본다.

curl -i -X POST https://example.com/api/users \
  -H 'Content-Type: application/json' \
  -d '{"email":"not-an-email"}'

-i 옵션으로 상태 코드와 헤더까지 같이 본다. body만 보면 상태 코드 확인을 빠뜨리기 쉽다.

에러 코드를 상수로 관리하고 있다면, 코드 목록을 API 문서에 같이 적어두는 게 나중에 유용하다. 같은 코드를 여러 엔드포인트에서 쓸 때 일관성을 유지하기도 쉽다.