--- id: api-rest-best-practices title: REST Best Practices — Resource / 상태코드 / HATEOAS category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [api, rest, http, vibe-coding] tech_stack: { language: "TS", applicable_to: ["Backend"] } applied_in: [] aliases: [REST, RESTful, HTTP API, resource-oriented, CRUD, status code] --- # REST Best Practices > 일관된 REST = 사용자 학습 비용↓. **명사 resource + HTTP method + 표준 status code**. JSON-API / RFC 7807 / OpenAPI 같이. ## 📖 핵심 개념 - Resource: 명사 (`/orders`, `/users/:id/orders`). - HTTP method: GET / POST / PUT / PATCH / DELETE. - Status code: 의미 있게. - Idempotency: GET / PUT / DELETE = idempotent. ## 💻 코드 패턴 ### URL 구조 ``` ✅ Good GET /orders # list GET /orders/42 # single POST /orders # create PUT /orders/42 # full update PATCH /orders/42 # partial update DELETE /orders/42 # delete GET /users/u1/orders # nested (user 의 orders) ❌ Bad GET /getOrders POST /createOrder GET /orders?action=delete ``` ### Status code ``` 2xx Success: 200 OK - GET, PATCH, PUT 201 Created - POST (with Location header) 202 Accepted - async (job queued) 204 No Content - DELETE, PUT (no body) 4xx Client error: 400 Bad Request - validation failed 401 Unauthorized - 인증 필요 403 Forbidden - 권한 부족 404 Not Found - 자원 없음 409 Conflict - 중복 / version mismatch 422 Unprocessable - semantic error 429 Too Many - rate limit 5xx Server: 500 Internal - 모르는 에러 502 Bad Gateway - upstream 에러 503 Unavailable - 일시 down 504 Gateway Timeout ``` ### POST → 201 + Location ```ts app.post('/orders', async (req, res) => { const order = await createOrder(req.body); res.status(201) .location(`/orders/${order.id}`) .json(order); }); ``` ### Pagination ``` GET /orders?limit=20&cursor=abc → 응답: { "data": [...], "pagination": { "next_cursor": "xyz", "has_more": true } } ``` 또는 `Link` 헤더: ``` Link: <...?cursor=xyz>; rel="next", <...?cursor=first>; rel="first" ``` → Cursor pagination 권장 (offset 보다 안정). ### Filtering / sorting / fields ``` GET /orders?status=paid&user_id=u1 GET /orders?sort=-createdAt GET /orders?fields=id,status,total # sparse fieldset GET /orders?include=items,customer # related ``` ### Versioning ``` URL: /v1/orders, /v2/orders Header: Accept: application/vnd.acme.v2+json Query: /orders?version=2 → URL 가 명확하고 cache 친화. 권장. ``` ### Error format (RFC 7807) ```ts res.status(400).json({ type: 'https://api.acme.com/errors/validation', title: 'Invalid input', status: 400, detail: 'Email must be valid', errors: [ { path: 'email', message: 'invalid format' }, { path: 'age', message: 'must be positive' } ], traceId: req.headers['x-request-id'], }); ``` → 일관 error envelope. ### Idempotency ``` GET : 항상 idempotent PUT : idempotent (전체 교체) DELETE : idempotent PATCH : 보통 X (depends) POST : X (단, idempotency-key header 로) ``` ``` POST /payments Idempotency-Key: 550e8400-... ``` → 같은 key 두 번 = 한 번만 처리. ### HATEOAS (controversial) ```json { "id": 42, "status": "shipped", "_links": { "self": "/orders/42", "cancel": "/orders/42/cancel", "shipment": "/shipments/99" } } ``` → Client 가 URL hardcode X. 그러나 거의 안 씀 — overkill. ### Authentication ``` Authorization: Bearer # 표준 또는 Authorization: Basic # legacy 또는 X-API-Key: # custom (덜 권장) ``` ### Rate limit headers ``` X-RateLimit-Limit: 1000 X-RateLimit-Remaining: 950 X-RateLimit-Reset: 1715238000 Retry-After: 30 # 429 시 ``` ### Cache headers ``` Cache-Control: public, max-age=60 ETag: "abc123" Last-Modified: ... # Client 가 다음 요청 If-None-Match: "abc123" → 304 Not Modified (body 없음) ``` ### Date / time ``` ISO 8601 UTC: "createdAt": "2026-05-09T10:30:00.000Z" ❌ "createdAt": "May 9, 2026 10:30 AM" ❌ "createdAt": 1715238000 # epoch — 의미 모름 ``` ### Money ```json { "amount": "1234.56", // string, 정확 "currency": "USD" } ``` → Float 사용 X. Decimal string. ### Bulk ``` POST /orders/bulk Body: [order1, order2, ...] → 부분 실패 처리: { "results": [ { "status": "ok", "id": "o1" }, { "status": "error", "error": {...} } ] } ``` ## 🤔 의사결정 기준 | 상황 | 권장 | |---|---| | Public API | REST + OpenAPI | | Internal microservice | gRPC / GraphQL / REST 어떤 것도 | | 복잡 query | GraphQL | | Realtime | WebSocket / SSE | | 강 type | tRPC (TS only) | | File upload | REST (multipart/form-data) | | Bulk | POST /resource/bulk | ## ❌ 안티패턴 - **GET 으로 변경**: 위험 (브라우저 prefetch 등). - **Verb URL (`/createOrder`)**: 명사 resource. - **모두 200 + body 안 error**: status code 의미 없음. - **500 으로 모든 에러**: 의미 잃음. - **PII URL**: log leak. body 또는 header. - **시간 하드코딩 timezone**: UTC ISO. - **versioning 없음**: breaking change 시 panic. - **Pagination 없는 list**: 1만 row 반환. ## 🤖 LLM 활용 힌트 - 명사 resource + HTTP method + 표준 status. - RFC 7807 error envelope. - OpenAPI 로 schema 명시. ## 🔗 관련 문서 - [[API_OpenAPI_Spec]] - [[API_Error_Format_RFC7807]] - [[API_Pagination_Patterns]] - [[API_Versioning_Strategies]]