[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
---
|
||||
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 <token> # 표준
|
||||
또는
|
||||
Authorization: Basic <base64> # legacy
|
||||
또는
|
||||
X-API-Key: <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]]
|
||||
Reference in New Issue
Block a user