| name | api-design |
| description | Use when designing API endpoints, reviewing API contracts, adding pagination/filtering, or planning versioning strategy. Do NOT use for API consumption, client-side HTTP, or GraphQL. |
API Design Patterns
ํ๋จ ๊ธฐ์ค๊ณผ ๊ท์น ์ค์ฌ. REST API ์ค๊ณ์ ์ฌ๋ฐ๋ฅธ ๊ฒฐ์ ์ ์๋ด.
Quick Start
CRITICAL Rules
- ALWAYS plural nouns for resources --
/users NOT /user
- NEVER verbs in URLs --
/users/:id NOT /getUser/:id (์์ธ: actions /orders/:id/cancel)
- ALWAYS kebab-case for multi-word URLs --
/team-members NOT /team_members
- ALWAYS use HTTP status codes semantically -- 200 for everything ๊ธ์ง
- ALWAYS
201 Created + Location header for POST -- ๋ฆฌ์์ค URL ๋ฐํ
- NEVER expose internal details in errors -- stack trace, SQL ์ฟผ๋ฆฌ ๊ธ์ง
- ALWAYS validate input with schema -- Zod, Pydantic, Bean Validation
- ALWAYS pagination for list endpoints -- unbounded list ๊ธ์ง
- PREFER cursor-based pagination for public APIs -- consistent performance
- ALWAYS rate limiting -- Anonymous, Authenticated, Premium ํฐ์ด ๋ถ๋ฆฌ
Resource Design
# Standard CRUD
GET /api/v1/users # List
GET /api/v1/users/:id # Get
POST /api/v1/users # Create
PUT /api/v1/users/:id # Full replace
PATCH /api/v1/users/:id # Partial update
DELETE /api/v1/users/:id # Delete
# Sub-resources (ownership)
GET /api/v1/users/:id/orders # User's orders
POST /api/v1/users/:id/orders # Create user's order
# Actions (verbs, sparingly)
POST /api/v1/orders/:id/cancel
POST /api/v1/auth/login
Method Semantics
| Method | Idempotent | Safe | Use For |
|---|
| GET | Yes | Yes | Retrieve |
| POST | No | No | Create, trigger action |
| PUT | Yes | No | Full replacement |
| PATCH | No* | No | Partial update |
| DELETE | Yes | No | Remove |
Status Code Decision
Request successful?
+-- Resource returned --> 200 OK
+-- Resource created --> 201 Created + Location header
+-- Accepted but processing later --> 202 Accepted
+-- No body to return --> 204 No Content
Client error?
+-- Malformed JSON/syntax --> 400 Bad Request
+-- Not authenticated --> 401 Unauthorized
+-- Authenticated but forbidden --> 403 Forbidden
+-- Resource not found --> 404 Not Found
+-- Duplicate/conflict --> 409 Conflict
+-- Valid JSON but invalid data --> 422 Unprocessable Entity
+-- Rate limit exceeded --> 429 Too Many Requests
Server error?
+-- Unexpected failure --> 500 (never expose details)
+-- Upstream failed --> 502 Bad Gateway
+-- Temporary overload --> 503 + Retry-After header
Common Mistakes
| Mistake | Fix |
|---|
200 + {"success": false} | Use proper HTTP status code |
| 500 for validation errors | 400 or 422 |
| 200 for created resource | 201 + Location |
| Stack trace in error response | Generic message + error code |
| 404 for authorization failure | 403 (authenticated) or 401 (not) |
Response Format
Standard Envelope
{ "data": { "id": "abc-123", "name": "Alice" } }
{ "data": [...], "meta": { "total": 142, "page": 1, "per_page": 20 } }
{ "error": { "code": "validation_error", "message": "...", "details": [...] } }
Rule: Public API๋ envelope (data wrapper) ์ฌ์ฉ. Internal API๋ flat response OK (status code๋ก ๊ตฌ๋ถ).
Error Response Rules
code: machine-readable (snake_case) -- not_found, validation_error
message: human-readable -- ์ฌ์ฉ์์๊ฒ ๋ณด์ฌ์ค ์ ์๋ ๋ฉ์์ง
details: field-level errors (validation) -- [{"field": "email", "message": "...", "code": "..."}]
Pagination Decision
| Use Case | Type | Why |
|---|
| Admin dashboard, <10K rows | Offset | "Jump to page N" ๊ฐ๋ฅ |
| Infinite scroll, feeds, large data | Cursor | Position-independent performance |
| Public API | Cursor (default) | Stable with concurrent writes |
| Search results | Offset | Users expect page numbers |
Cursor-Based Pattern
{
"data": [...],
"meta": { "has_next": true, "next_cursor": "eyJpZCI6MTQzfQ" }
}
Offset-Based Pattern
{
"data": [...],
"meta": { "total": 142, "page": 1, "per_page": 20, "total_pages": 8 },
"links": { "next": "/api/v1/users?page=2&per_page=20" }
}
Filtering, Sorting, Search
# Filtering
GET /api/v1/orders?status=active&customer_id=abc-123
GET /api/v1/products?price[gte]=10&price[lte]=100
GET /api/v1/products?category=electronics,clothing
# Sorting (prefix - for DESC)
GET /api/v1/products?sort=-created_at,price
# Search
GET /api/v1/products?q=wireless+headphones
# Sparse fieldsets (reduce payload)
GET /api/v1/users?fields=id,name,email
Versioning Decision
Need versioning?
+-- First API? --> Start with /api/v1/, don't version until needed
+-- Breaking change?
| +-- Removing/renaming fields --> New version required
| +-- Changing field types --> New version required
| +-- Changing URL structure --> New version required
+-- Non-breaking change?
+-- Adding new response fields --> No new version
+-- Adding optional query params --> No new version
+-- Adding new endpoints --> No new version
| Strategy | Pros | Cons | Recommendation |
|---|
URL path (/v1/) | Explicit, cacheable | URL changes | Recommended |
Header (Accept: vnd.app.v2+json) | Clean URLs | Hidden, hard to test | Internal APIs |
Deprecation process:
- Announce (6 months for public APIs)
- Add
Sunset header
- Return
410 Gone after sunset date
- Maintain max 2 versions (current + previous)
Security Boundaries
Validation ์์น ๊ท์น
๋ด๋ถ ์ฝ๋๋ ํ์
๊ณ์ฝ์ ์ ๋ขฐํ๊ณ , ์ธ๋ถ์์ ๋ค์ด์ค๋ ๊ฐ๋ง ๊ฒ์ฆํ๋ผ.
| ๊ฒ์ฆํด์ผ ํ ์์น | ์ด์ |
|---|
| API route/controller handler (์ฌ์ฉ์ ์
๋ ฅ) | ์ธ๋ถ ์
๋ ฅ = ์ ๋ขฐ ๋ถ๊ฐ |
| ํผ ์ ์ถ ํธ๋ค๋ฌ | ๋์ผ |
| ์ธ๋ถ ์๋น์ค ์๋ต ํ์ฑ | ์จ๋ํํฐ๋ ์ ๋ขฐ ๋ถ๊ฐ |
| ํ๊ฒฝ๋ณ์/์ค์ ๋ก๋ฉ | ์ค์ ์ค๋ฅ ์กฐ๊ธฐ ๋ฐ๊ฒฌ |
| ๊ฒ์ฆํ๋ฉด ์ ๋๋ ์์น | ์ด์ |
|---|
| ์ด๋ฏธ ๊ฒ์ฆ๋ ๋ฐ์ดํฐ๋ฅผ ๋ฐ๋ ๋ด๋ถ ํจ์ | ์ค๋ณต, ์ฑ๋ฅ ์์ค |
| DB์์ ๋ง ์ฝ์ ๋ฐ์ดํฐ | ๋ด ์์คํ
์ด ์ผ์ผ๋ ์ ๋ขฐ |
| ์ ํธ ํจ์ (๊ฒ์ฆ๋ ํธ์ถ์๊ฐ ํธ์ถ) | ๋์ผ |
์จ๋ํํฐ ์๋ต = ์ ๋ขฐํ ์ ์๋ ์
๋ ฅ
์ธ๋ถ API ์๋ต์ ์ฌ์ฉ์ ์
๋ ฅ๊ณผ ๋์ผํ๊ฒ ๋ค๋ค์ผ ํ๋ค.
๊ฐ์ผ๋๊ฑฐ๋ ์ค๋์ํ๋ ์ธ๋ถ ์๋น์ค๋ ์์ ๋ฐ ํ์
, ์
์์ ์ฝํ
์ธ , ํ๋กฌํํธ ์ธ์ ์
ํ ํ
์คํธ๋ฅผ ๋ฐํํ ์ ์๋ค. ๋ ๋๋งยท๋น์ฆ๋์ค ๋ก์งยทํ๋จ์ ์ฌ์ฉํ๊ธฐ ์ ์ ํํ์ ๋ด์ฉ์ ๋ชจ๋ ๊ฒ์ฆํ๋ผ.
- Spring: WebClient ์๋ต โ DTO ๋งคํ +
@Valid ๋๋ ์๋ ๊ฒ์ฆ
- Python: Pydantic์ผ๋ก ์ธ๋ถ ์๋ต๋ ํ์ฑ (
httpx response โ Model.model_validate)
- Node: Zod
safeParse ํ ์ฌ์ฉ
Rate Limiting
| Tier | Limit | Window | Headers |
|---|
| Anonymous | 30/min | Per IP | X-RateLimit-Limit, X-RateLimit-Remaining |
| Authenticated | 100/min | Per user | + X-RateLimit-Reset |
| Premium | 1000/min | Per API key | Same |
When exceeded: 429 Too Many Requests + Retry-After header.
API Design Checklist
Before shipping:
Common Rationalizations
์ฝ๋ ๋ฆฌ๋ทฐ์์ ์์ฃผ ๋์ค๋ ๋ณ๋ช
๊ณผ ๋ฐ๋ฐ. ๋ ์์ ์ ์ค๊ณ ๋ฆฌ๋ทฐ์๋ ์ ์ฉํ๋ผ.
| ๋ณ๋ช
| ๋ฐ๋ฐ |
|---|
| "๋์ค์ ๋ฌธ์ํํ ๊ฒ์" | ํ์
์ด ๊ณง ๋ฌธ์๋ค. DTO/์คํค๋ง๋ฅผ ๋จผ์ ์ ์ํ๋ฉด OpenAPI๊ฐ ์๋ ์์ฑ๋๋ค. |
| "์ง๊ธ์ pagination ํ์ ์์ด์" | 100๊ฐ ๋๋ ์๊ฐ ํ์ํด์ง๋ค. ๋ ๊ฑฐ์ ์๋ํฌ์ธํธ์ pagination ์ถ๊ฐํ๋ ๊ฒ 3๋ฐฐ ํ๋ค๋ค. |
| "PATCH ๋ณต์กํ๋ PUT์ผ๋ก ํต์ผํ์ฃ " | PUT์ ๋งค๋ฒ ์ ์ฒด ๊ฐ์ฒด๋ฅผ ์๊ตฌํ๋ค. ํด๋ผ์ด์ธํธ๊ฐ ์ค์ ๋ก ์ํ๋ ๊ฑด PATCH๋ค. |
| "๋ฒ์ ๋์ ํ์ํด์ง ๋ ํ์ฃ " | versioning ์๋ breaking change = ์๋น์ ํ๊ดด. v1/ ํ๋ฆฌํฝ์ค๋ง ๋จผ์ ๋ฐ์๋ ๋น์ฉ์ด ๊ฑฐ์ ์๋ค. |
| "์๋ฌด๋ ๊ทธ ๋ฏธ๋ฌธ์ํ๋ ๋์ ์ ์จ์" | Hyrum's Law. ๊ด์ฐฐ ๊ฐ๋ฅํ ๊ฑด ๋๊ตฐ๊ฐ ์์กดํ๋ค. ํ
์คํธ ์ปค๋ฒ๋ฆฌ์ง์ ๋ฌด๊ด. |
| "๋ด๋ถ API๋ ๊ณ์ฝ ํ์ ์์ด์" | ๋ด๋ถ ์๋น์๋ ์๋น์. ๊ณ์ฝ ์์ด๋ ํ ๊ฐ ๋ณ๋ ฌ ์์
๋ถ๊ฐ๋ฅ. |
| "Controller์์ Entity ๋ฐ๋ก ๋ฐํํด๋ ๋ผ์" (Spring) | Entity ๋
ธ์ถ = ๋ด๋ถ ๊ตฌ์กฐ ์ ์ถ + Hyrum's Law. JPA proxy ์ง๋ ฌํ ์ LazyInitializationException๊น์ง ํฐ์ง๋ค. DTO ๋ถ๋ฆฌ๋ ํ์ ๋ถ๊ฐ. |
| "๋ด๋ถ์ฉ์ด๋ผ rate limiting ์ ํด๋ ๋ผ์" | ๋ด๋ถ ๋ฐฐ์น ์์
์ด prod DB ํฐ๋จ๋ฆฌ๋ ์ฌ๋ก๊ฐ ๊ฐ์ฅ ํํ๋ค. ๊ฐ์ ํ์ฌ ๋ค๋ฅธ ํ๋ ์ค์๋ก "๊ณต๊ฒฉ์"๊ฐ ๋๋ค. |
| "ํ๋ก ํธ๊ฐ ๊ฒ์ฆํ๋ ์๋ฒ๋ ์๋ตํด๋ ๋ผ์" | ํ๋ก ํธ ๊ฒ์ฆ์ UX, ์๋ฒ ๊ฒ์ฆ์ ๋ณด์. curl ํ ๋ฒ์ด๋ฉด ํ๋ก ํธ ์ฐํ. ๋์ ์๋ก๋ฅผ ์ ๋ ๋์ฒดํ ์ ์๋ค. |
Cross-References
| Topic | Skill |
|---|
| Spring Boot REST controller, exception handling | springboot-patterns |
| ์ธ์ด๋ณ ๊ตฌํ ์์ (TypeScript, Python, Go, Spring) | references/implementation-examples.md |
| SQL ํ์ด์ง๋ค์ด์
์ต์ ํ | sql-optimization-patterns |
| REST/GraphQLยทauth ๋ฐฉ์ ๋ฑ ์ํคํ
์ฒ ๊ฒฐ์ ๊ธฐ๋ก | adr |
References