DELETE /deleteUser/123 — 이런 URL을 짠 적 있다면 이 글이 필요하다. REST API 설계에는 규칙이 있다. 지키지 않아도 돌아가지만, 프론트와 협업하고 팀이 커질수록 이 차이가 유지보수 비용으로 돌아온다. 처음 짤 때 놓치는 것들을 항목별로 정리했다.
1. URL은 명사, 복수형, 소문자
가장 흔한 실수는 URL에 동사를 쓰는 것이다. HTTP 메서드가 이미 동사 역할을 한다. URL은 리소스(명사)만 표현하면 된다.
// 잘못된 예
GET /getUsers
POST /createPost
DELETE /deleteUser/123
GET /user_profile/123
// 올바른 예
GET /users
POST /posts
DELETE /users/123
GET /users/123/profile
복수형을 쓰는 이유는 일관성이다. /user/1과 /users를 혼용하면 API를 쓰는 쪽에서 매번 확인해야 한다. 처음부터 복수형으로 고정하면 예측 가능해진다. 단어 구분은 언더스코어(_) 대신 하이픈(-)으로, 전체 소문자를 유지한다.
계층 관계는 중첩 경로로 표현한다. /users/123/posts는 “123번 유저의 게시글 목록”이다. 중첩이 3단계 이상 넘어가면 쿼리 파라미터로 푸는 게 낫다.
2. HTTP 메서드를 목적에 맞게
// 잘못된 예 — 전부 POST로 처리
POST /api/getUser
POST /api/deleteUser
POST /api/updateUserName
// 올바른 예 — 메서드로 의미 전달
GET /users/123 // 조회
POST /users // 생성
PUT /users/123 // 전체 수정
PATCH /users/123 // 부분 수정
DELETE /users/123 // 삭제
모든 요청을 POST로 처리하면 동작은 하지만 HTTP 캐싱을 쓸 수 없고, 네트워크 레이어에서 요청의 의도를 알 수 없다.
PUT과 PATCH의 차이가 헷갈리면 이렇게 판단하면 된다. PUT은 보내지 않은 필드가 null로 덮어써진다. PATCH는 보낸 필드만 수정하고 나머지는 그대로 둔다. 일반적인 “수정” API는 PATCH가 맞다.
3. 상태 코드는 정확하게
성공이든 실패든 200을 반환하고 body에 success: false를 담는 패턴이 있다. 이러면 HTTP 레이어에서 에러를 감지할 수 없어서 로깅, 모니터링, 클라이언트 처리가 전부 복잡해진다.
// 잘못된 예
HTTP 200
{ "success": false, "message": "User not found" }
// 올바른 예
HTTP 404
{ "error": "USER_NOT_FOUND", "message": "해당 사용자가 존재하지 않습니다." }
자주 쓰는 상태 코드는 정해져 있다.
| 상태 코드 | 의미 | 언제 |
|---|---|---|
| 200 OK | 성공 | GET 조회, PUT/PATCH 수정 |
| 201 Created | 생성 완료 | POST로 리소스 생성 |
| 204 No Content | 성공, 응답 없음 | DELETE 성공 |
| 400 Bad Request | 요청 형식 오류 | 필수 파라미터 누락 |
| 401 Unauthorized | 인증 없음 | 토큰 없음 또는 만료 |
| 403 Forbidden | 권한 없음 | 인증됐지만 접근 불가 |
| 404 Not Found | 리소스 없음 | 존재하지 않는 ID |
| 409 Conflict | 충돌 | 이미 존재하는 이메일 |
| 422 Unprocessable | 유효성 실패 | 형식은 맞는데 값이 잘못됨 |
| 500 Internal Error | 서버 오류 | 예상치 못한 서버 에러 |
401과 403의 차이가 헷갈린다면 별도로 정리된 글을 참고하자.
4. 응답 구조를 일관되게
엔드포인트마다 응답 형식이 다르면 프론트에서 파싱 로직을 따로 짜야 한다. API가 10개, 20개로 늘어날수록 이 비용이 눈덩이처럼 불어난다.
// 잘못된 예 — 엔드포인트마다 다른 포맷
GET /users/1 → { id: 1, name: "Kim" }
GET /posts/1 → { data: { id: 1 }, status: "ok" }
GET /comments → [{ id: 1 }]
// 올바른 예 — 일관된 구조
// 단건 조회
{
"data": { "id": 1, "name": "Kim" }
}
// 목록 조회
{
"data": [...],
"meta": { "total": 100, "page": 1, "limit": 20 }
}
배열을 바로 반환하지 않는 이유가 있다. 나중에 메타 정보(페이지네이션, 총 개수)를 추가해야 할 때 배열 응답이면 구조 자체를 바꿔야 한다. 처음부터 data 키로 감싸두면 하위 호환을 깰 일이 없다.
5. 에러 응답에도 포맷이 있어야 한다
에러가 발생했을 때 응답 형식이 없으면 프론트가 에러 메시지를 파싱하는 로직을 if문으로 덕지덕지 짜게 된다. 에러 응답은 처음부터 구조를 고정해두는 게 낫다.
// 권장 에러 응답 구조
{
"error": {
"code": "VALIDATION_FAILED", // 기계가 읽는 코드
"message": "이메일 형식이 올바르지 않습니다.", // 사람이 읽는 메시지
"field": "email" // 어느 필드인지 (선택)
}
}
code 필드를 문자열 상수로 고정하면 프론트에서 if (error.code === 'UNAUTHORIZED') 형태로 처리할 수 있다. HTTP 상태 코드만으로 에러 종류를 충분히 구분하기 어려울 때 code가 역할을 한다.
6. 버저닝은 처음부터
처음에 버저닝 없이 시작했다가 /v2를 붙이는 순간 레거시 클라이언트 대응 문제가 생긴다. 처음부터 /v1/을 붙이는 게 나중 마이그레이션 고통을 줄인다.
// URL 버저닝 — 가장 일반적이고 디버깅하기 쉬움
GET /v1/users
GET /v2/users
// Header 버저닝 — URL은 깔끔하지만 테스트가 불편함
Accept: application/vnd.api+json;version=1
실무에서는 URL 버저닝이 압도적으로 많다. 브라우저 주소창에서 바로 확인 가능하고, CDN 캐싱 설정도 간단하다. 모놀리식과 마이크로서비스 구조에 따라 버저닝 전략이 달라질 수 있지만, 어떤 구조든 버저닝 자체는 필요하다.
API 서버 배포 시 서버 배포 전 확인 체크리스트에서 CORS 설정과 함께 버저닝 경로가 올바르게 라우팅되는지도 확인하자.
한눈에 보는 REST API 설계 체크리스트
| 항목 | 확인 포인트 |
|---|---|
| URL 설계 | 명사 + 복수형 + 소문자, 동사 없음 |
| HTTP 메서드 | GET/POST/PUT/PATCH/DELETE 목적에 맞게 |
| 상태 코드 | 성공/실패/에러 종류별 정확한 코드 |
| 응답 구조 | 모든 엔드포인트 동일한 포맷 |
| 에러 응답 | code + message 포함한 일관된 구조 |
| 버저닝 | /v1/부터 시작 |
| 목록 응답 | 배열 직접 반환 말고 data + meta 감싸기 |
| DELETE 성공 | 204 No Content 반환 |
MDN HTTP 상태 코드 레퍼런스에서 전체 상태 코드 목록을 확인할 수 있다.
REST API 설계는 처음 한 번 잘 잡으면 나중에 고칠 일이 없다. URL 동사, 상태 코드 200 통일, 엔드포인트마다 다른 응답 포맷 — 이 세 가지만 피해도 협업 비용이 절반으로 준다.