RESTful API 설계: 확장 가능한 API 구축을 위한 모범 사례
Source: Dev.to
번역할 텍스트를 제공해 주시면 한국어로 번역해 드리겠습니다.
리소스 명명
✅ 좋음 – 리소스는 명사
GET /users # List users
POST /users # Create user
GET /users/123 # Get user
PUT /users/123 # Update user
DELETE /users/123 # Delete user
❌ 나쁨 – URL에 동사 사용
GET /getUsers
POST /createUser
GET /getUserById/123
관계를 위한 중첩 리소스
GET /users/123/orders # User's orders
GET /users/123/orders/456 # Specific order
POST /users/123/orders # Create order for user
대안: 필터링을 위한 쿼리 파라미터
GET /orders?user_id=123 # Filter orders by user
경험 법칙: 컬렉션에는 복수형 명사(
/users)를 사용하고, 특정 리소스에는 단수형 식별자(/users/123)를 사용합니다.
명명 규칙
-
Kebab‑case – 여러 단어로 이루어진 리소스
GET /user-profiles GET /order-items GET /shipping-addresses -
snake_case – 쿼리 파라미터
GET /products?category_id=5&sort_by=price&sort_order=desc
HTTP 메서드
| 메서드 | 목적 | 멱등성? | 캐시 가능성? |
|---|---|---|---|
GET | 리소스 조회 | ✅ | ✅ |
POST | 새로운 리소스 생성 | ❌ | ❌ |
PUT | 전체 리소스 교체 | ✅ | ❌ |
PATCH | 부분 업데이트 | ❌ | ❌ |
DELETE | 리소스 삭제 | ✅ | ❌ |
OPTIONS | 허용된 메서드 탐색 | ✅ | ✅ |
HEAD | 헤더만 조회 | ✅ | ✅ |
상태 코드
성공
200 OK # 성공적인 GET, PUT, PATCH
201 Created # 성공적인 POST (Location 헤더 포함)
204 No Content # 성공적인 DELETE
클라이언트 오류
400 Bad Request # 잘못된 요청 본문/매개변수
401 Unauthorized # 누락되었거나 잘못된 인증
403 Forbidden # 인증은 되었지만 권한이 없음
404 Not Found # 리소스가 존재하지 않음
409 Conflict # 리소스 충돌 (중복 등)
422 Unprocessable Entity # 검증 오류
429 Too Many Requests # 속도 제한 초과
서버 오류
500 Internal Server Error # 예상치 못한 서버 오류
502 Bad Gateway # 상위 서비스 오류
503 Service Unavailable # 일시적 과부하
Response Payloads
Single‑resource success
{
"data": {
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"created_at": "2024-01-15T10:30:00Z"
},
"meta": {
"request_id": "req_abc123"
}
}
Collection response with pagination
{
"data": [
{ "id": 1, "name": "Product A" },
{ "id": 2, "name": "Product B" }
],
"meta": {
"total": 150,
"page": 1,
"per_page": 20,
"total_pages": 8
},
"links": {
"self": "/products?page=1",
"next": "/products?page=2",
"last": "/products?page=8"
}
}
Error response
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request contains invalid data",
"details": [
{ "field": "email", "message": "Invalid email format" },
{ "field": "age", "message": "Must be at least 18" }
]
},
"meta": {
"request_id": "req_xyz789"
}
}
팁: 디버깅 및 지원을 위해 응답에 항상
request_id(또는 유사한 값)를 포함하세요.
쿼리 기반 기능
필터링
GET /products?category=electronics&brand=apple&min_price=100&max_price=500
정렬
GET /products?sort=price:asc,created_at:desc
페이지네이션
-
오프셋 기반
GET /products?page=2&per_page=20 -
커서 기반 (대용량 데이터셋에 더 적합)
GET /products?cursor=eyJpZCI6MTAwfQ&limit=20
희소 필드셋 (필드 선택)
GET /users/123?fields=id,name,email
연관 리소스 포함
GET /orders/123?include=user,items,shipping_address
버전 관리
GET /v1/users
GET /v2/users
헤더 기반 버전 관리 (권장)
GET /users
Accept: application/vnd.myapi.v2+json
폐기 헤더 (구버전용)
{
"headers": {
"Deprecation": "Sun, 01 Jan 2025 00:00:00 GMT",
"Sunset": "Sun, 01 Jul 2025 00:00:00 GMT",
"Link": "; rel=\"successor-version\""
}
}
인증 및 권한 부여
베어러 토큰 (Authorization 헤더)
GET /api/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
JWT 페이로드 예시
{
"header": { "alg": "RS256", "typ": "JWT" },
"payload": {
"sub": "user_123",
"email": "user@example.com",
"roles": ["user", "admin"],
"iat": 1704067200,
"exp": 1704153600
}
}
API‑키 (헤더 – 권장)
GET /api/data
X-API-Key: sk_live_abc123xyz
API‑키 (쿼리 – 보안 수준 낮음)
GET /api/data?api_key=sk_live_abc123xyz
세분화된 권한 모델
read:users– 사용자 데이터 읽기write:users– 사용자 생성 / 업데이트delete:users– 사용자 삭제read:orders– 주문 데이터 읽기admin– 전체 접근 권한
스코프가 포함된 토큰
{
"access_token": "...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read:users read:orders"
}
Rate Limiting
성공적인 응답 헤더
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1704067200
속도 제한이 적용된 경우
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1704067200
슬라이딩‑윈도우 구현 (예시)
const rateLimiter = {
// Per‑user limits
authenticated: {
requests: 1000,
window: '1 hour'
},
// Per‑IP limits for unauthenticated callers
anonymous: {
requests: 100,
window: '1 hour'
},
// Per‑endpoint limits
endpoints: {
'POST /auth/login': { requests: 5, window: '1 minute' },
'POST /users': { requests: 10, window: '1 hour' }
}
};
구조화된 오류 카탈로그
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "The requested user was not found",
"details": {
"resource_type": "User",
"resource_id": "123"
},
"documentation_url": "https://api.example.com/docs/errors#RESOURCE_NOT_FOUND",
"request_id": "req_abc123"
}
}
중앙 집중식 오류‑코드 맵 (예시)
const errorCodes = {
// Authentication errors (1xxx)
INVALID_CREDENTIALS: { status: 401, message: 'Invalid email or password' },
TOKEN_EXPIRED: { status: 401, message: 'Authentication token has expired' },
INSUFFICIENT_PERMISSIONS: { status: 403, message: 'Insufficient permissions' },
// Validation errors (2xxx)
VALIDATION_ERROR: { status: 422, message: 'Validation failed' },
// Resource errors (3xxx)
RESOURCE_NOT_FOUND: { status: 404, message: 'Resource not found' },
CONFLICT: { status: 409, message: 'Resource conflict' },
// Rate‑limit errors (4xxx)
TOO_MANY_REQUESTS: { status: 429, message: 'Rate limit exceeded' },
// Server errors (5xxx)
INTERNAL_ERROR: { status: 500, message: 'Internal server error' }
};
빠른 체크리스트
- Resources: 명사는 컬렉션에 대해 복수형, 항목에 대해 단수형 ID 사용.
- Naming: URL은 kebab‑case, 쿼리 파라미터는 snake_case 사용.
- Methods: 올바른 HTTP 메서드 사용; 적절한 경우 멱등성을 유지.
- Status codes: 상황에 가장 구체적인 코드를 반환.
- Responses: 페이지네이션 시
data,meta(request_id포함),links포함. - Filtering / Sorting / Pagination: 쿼리 파라미터를 통해 노출.
- Versioning: URL 또는 media‑type 버전 관리; 적절한 헤더로 폐기 알림.
- Auth: 헤더 기반 토큰 또는 API 키 선호; 스코프/역할 포함.
- Rate limiting: 응답 헤더를 통해 제한 전달; 슬라이딩 윈도우 구현.
- Errors: 일관된 JSON 구조, 오류 코드, 문서 링크, 요청 ID 포함.
이 가이드라인을 따라 명확하고, 일관되며, 사용하기 쉬운 API를 구축하세요.
오류 정의
RMISSIONS: { status: 403, message: 'You do not have permission' },
// Validation errors (2xxx)
VALIDATION_ERROR: { status: 422, message: 'Request validation failed' },
INVALID_FORMAT: { status: 400, message: 'Invalid request format' },
// Resource errors (3xxx)
RESOURCE_NOT_FOUND: { status: 404, message: 'Resource not found' },
RESOURCE_CONFLICT: { status: 409, message: 'Resource already exists' },
// Rate limiting (4xxx)
RATE_LIMIT_EXCEEDED: { status: 429, message: 'Too many requests' },
// Server errors (5xxx)
INTERNAL_ERROR: { status: 500, message: 'An unexpected error occurred' },
SERVICE_UNAVAILABLE: { status: 503, message: 'Service temporarily unavailable' },
};
샘플 HAL‑스타일 응답
{
"data": {
"id": 123,
"status": "pending",
"total": 99.99
},
"links": {
"self": { "href": "/orders/123" },
"customer": { "href": "/users/456" },
"items": { "href": "/orders/123/items" }
},
"actions": {
"cancel": {
"href": "/orders/123/cancel",
"method": "POST",
"title": "Cancel this order"
},
"pay": {
"href": "/orders/123/pay",
"method": "POST",
"title": "Process payment"
}
}
}
OpenAPI 사양 (YAML)
openapi: 3.0.3
info:
title: My API
version: 1.0.0
description: A sample API
paths:
/users:
get:
summary: List all users
parameters:
- name: page
in: query
schema:
type: integer
default: 1
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string
email:
type: string
format: email
API 설계 모범 사례
잘 설계된 API는 직관적이고 일관되며 확장성이 뛰어납니다. 적절한 리소스 명명, 알맞은 상태 코드 사용, 포괄적인 오류 처리, 명확한 문서화를 포함한 이러한 모범 사례를 따르면 개발자들이 사용하기 좋아하는 API를 만들 수 있습니다.
핵심 요점
- 리소스에는 명사를 사용하고, 동작에는 HTTP 메서드를 사용합니다.
- 적절한 상태 코드를 반환합니다.
- 처음부터 올바른 버전 관리를 구현합니다.
- 포괄적인 오류 응답을 설계합니다.
- 모든 것을 OpenAPI로 문서화합니다.