API 응답은 어떤 구조를 써야 하나
핵심 요약: 맨 객체는 메타데이터가 필요할 때까지 깔끔하고, 완전한 봉투는 안전하지만 장황합니다. 대부분 프로젝트에는 결과에 HTTP 상태를, 데이터에 안정적 봉투를 쓰세요.

핵심부터 말하면, 결과를 4xx/5xx 상태 코드로 표현하고 페이로드를 { "data": ..., "meta": ... }에 담으며, 실패 시 error 봉투로 전환하세요. 내부 API에는 그거면 충분하고, 통합자가 많은 공개 플랫폼만 JSON:API나 OpenAPI가 필요합니다.
최악의 선택을 먼저 배제하세요. 항상 200을 반환하고 결과를 code 필드에 넣는 것.
세 가지 모양
A. 맨 객체
{ "id": 1, "name": "demo", "price": 99 }
깔끔하고 HTTP 의미가 일을 합니다(404는 404). 그러나 페이지네이션, 경고, 추적 id가 필요한 순간 계약을 바꾸지 않고는 둘 곳이 없습니다.
B. 안정적 봉투
{
"data": { "id": 1, "name": "demo" },
"meta": { "requestId": "abc", "took": 12 }
}
페이로드와 메타데이터가 분리되어 새 메타데이터가 data를 재구성하지 않습니다. 중첩 한 단계가 비용입니다.
C. 전부 봉투(피하세요)
{ "code": 0, "message": "success", "data": {...} }
모양이 죄는 아닙니다. 관행이 죄입니다. 모든 결과에 상태 200을 쓰면 CDN, 게이트웨이, 회로 차단기, 재시도 정책, 대시보드가 모두 성공을 봅니다. 그러면 모든 경고에 맞춤 파싱이 필요합니다.
권장
결과는 HTTP 상태, 페이로드는 봉투.
{
"data": { "id": 1, "name": "demo" },
"meta": { "requestId": "req_8f2a", "took": 12 }
}
{
"error": {
"code": "ORDER_NOT_FOUND",
"message": "Order does not exist or was deleted",
"details": [{ "field": "id", "issue": "not_found" }]
},
"meta": { "requestId": "req_8f2a" }
}
상태 4xx나 5xx, code는 안정적이고 기계 가독, message는 사람용, details는 필드 수준 폼 오류용.
하드 규칙
- 목록 엔드포인트는 페이지네이션 메타데이터를 담아야 합니다.
total이나 커서가 없으면 페이지징이 없습니다. - 빈 목록은
[]를 반환하고 절대null이 아닙니다.null.map은 반복되는 장애입니다. - 타임스탬프는 오프셋이 있는 ISO 8601. 타임스탬프 형식을 참고하세요.
- 하나의 이름 관례.
snake_case나camelCase, 경계에서 한 번 변환합니다. - 돈은 정수 최소 단위나 문자열.
99.99는 이진 부동소수점에서 정확하지 않습니다. null과 누락을 혼동하지 마세요.null인가 필드 생략인가를 참고하세요.
명세를 도입할 때
- 통합자가 많은 공개 플랫폼: JSON:API 또는 OpenAPI + 공유 봉투
- 단일 팀 내부 API: 위 봉투면 충분
- 강한 계약: Schema 우선, 구조를 그에서 생성
명세는 논의를 아끼고 온보딩 비용을 줄입니다. 다섯 명 팀에는 JSON:API가 대개 부정적인 작업입니다.
FAQ
맨 객체와 봉투 중 어떻게 고르나?
페이지네이션, 경고, requestId가 필요하면 봉투를 써서 meta가 data를 재구성하지 않고 자라게 하세요. 메타데이터가 전혀 필요 없고 나중에 필드를 더하면 계약이 바뀌는 것을 받아들일 수 있을 때만 맨 객체를 반환하세요.
API가 왜 모든 결과에 200을 반환하면 안 되나?
CDN, 게이트웨이, 회로 차단기, 재시도 정책, 대시보드가 모두 HTTP 상태를 읽어 캐시, 재시도, 경고 여부를 결정합니다. 모든 곳이 200이면 실패를 성공으로 취급하고, 모든 경고에 업무 코드 맞춤 파싱이 필요합니다.
목록 엔드포인트가 페이지네이션 메타데이터를 담아야 하나?
예. total이나 커서가 없으면 페이지징이 없고, 빈 목록은 null이 아니라 []를 반환해야 프런트엔드의 .map 호출이 깨지지 않습니다.