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 문서에 같이 적어두는 게 나중에 유용하다. 같은 코드를 여러 엔드포인트에서 쓸 때 일관성을 유지하기도 쉽다.