445 lines
14 KiB
Markdown
445 lines
14 KiB
Markdown
---
|
|
name: fullstack-dev-api-design
|
|
description: "API design patterns and best practices. Use when creating endpoints, choosing methods/status codes, implementing pagination, or writing OpenAPI specs. Prevents common REST/GraphQL/gRPC mistakes."
|
|
license: MIT
|
|
metadata:
|
|
version: "2.0.0"
|
|
sources:
|
|
- Microsoft REST API Guidelines
|
|
- Google API Design Guide
|
|
- Zalando RESTful API Guidelines
|
|
- JSON:API Specification
|
|
- RFC 9457 (Problem Details for HTTP APIs)
|
|
- RFC 9110 (HTTP Semantics)
|
|
---
|
|
|
|
# API Design Guidelines
|
|
|
|
Framework-agnostic API design guide for backend and full-stack engineers. 50+ rules across 10 categories, prioritized by impact. Covers REST, GraphQL, and gRPC.
|
|
|
|
## Scope
|
|
|
|
**USE this skill when:**
|
|
- Designing a new API or adding endpoints
|
|
- Reviewing API pull requests
|
|
- Choosing between REST / GraphQL / gRPC
|
|
- Writing OpenAPI specifications
|
|
- Migrating or versioning an existing API
|
|
|
|
**NOT for:**
|
|
- Framework-specific implementation details (use your framework's own skill/docs)
|
|
- Frontend data fetching patterns (use React Query / SWR docs)
|
|
- Authentication implementation details (use your auth library's docs)
|
|
- Database schema design (→ `database-schema-design`)
|
|
|
|
## Context Required
|
|
|
|
Before applying this skill, gather:
|
|
|
|
| Required | Optional |
|
|
|----------|----------|
|
|
| Target consumers (browser, mobile, service) | Existing API conventions in the project |
|
|
| Expected request volume (RPS estimate) | Current OpenAPI / Swagger spec |
|
|
| Authentication method (JWT, API key, OAuth) | Rate limiting requirements |
|
|
| Data model / domain entities | Caching strategy |
|
|
|
|
---
|
|
|
|
## Quick Start Checklist
|
|
|
|
New API endpoint? Run through this before writing code:
|
|
|
|
- [ ] Resource named as **plural noun** (`/orders`, not `/getOrders`)
|
|
- [ ] URL in **kebab-case**, body fields in **camelCase**
|
|
- [ ] Correct **HTTP method** (GET=read, POST=create, PUT=replace, PATCH=partial, DELETE=remove)
|
|
- [ ] Correct **status code** (201 Created, 422 Validation, 404 Not Found…)
|
|
- [ ] Error response follows **RFC 9457** envelope
|
|
- [ ] **Pagination** on all list endpoints (default 20, max 100)
|
|
- [ ] **Authentication** required (Bearer token, not query param)
|
|
- [ ] **Request ID** in response header (`X-Request-Id`)
|
|
- [ ] **Rate limit** headers included
|
|
- [ ] Endpoint documented in **OpenAPI spec**
|
|
|
|
---
|
|
|
|
## Quick Navigation
|
|
|
|
| Need to… | Jump to |
|
|
|----------|---------|
|
|
| Name a resource URL | [1. Resource Modeling](#1-resource-modeling-critical) |
|
|
| Pick HTTP method + status code | [3. HTTP Methods & Status Codes](#3-http-methods--status-codes-critical) |
|
|
| Format error responses | [4. Error Handling](#4-error-handling-high) |
|
|
| Add pagination or filtering | [6. Pagination & Filtering](#6-pagination--filtering-high) |
|
|
| Choose API style (REST vs GraphQL vs gRPC) | [10. API Style Decision](#10-api-style-decision-tree) |
|
|
| Version an existing API | [7. Versioning](#7-versioning-medium-high) |
|
|
| Avoid common mistakes | [Anti-Patterns](#anti-patterns-checklist) |
|
|
|
|
---
|
|
|
|
## 1. Resource Modeling (CRITICAL)
|
|
|
|
### Core Rules
|
|
|
|
```
|
|
✅ /users — plural noun
|
|
✅ /users/{id}/orders — 1 level nesting
|
|
✅ /reviews?orderId={oid} — flatten deep nesting with query params
|
|
|
|
❌ /getUsers — verb in URL
|
|
❌ /user — singular
|
|
❌ /users/{uid}/orders/{oid}/items/{iid}/reviews — 3+ levels deep
|
|
```
|
|
|
|
**Max nesting: 2 levels.** Beyond that, promote to top-level resource with filters.
|
|
|
|
### Domain Alignment
|
|
|
|
Resources map to **domain concepts**, not database tables:
|
|
|
|
```
|
|
✅ /checkout-sessions (domain aggregate)
|
|
✅ /shipping-labels (domain concept)
|
|
|
|
❌ /tbl_order_header (database table leak)
|
|
❌ /join_user_role (internal schema leak)
|
|
```
|
|
|
|
---
|
|
|
|
## 2. URL & Naming (CRITICAL)
|
|
|
|
| Context | Convention | Example |
|
|
|---------|-----------|---------|
|
|
| URL path | kebab-case | `/order-items` |
|
|
| JSON body fields | camelCase | `{ "firstName": "Jane" }` |
|
|
| Query params | camelCase or snake_case (be consistent) | `?sortBy=createdAt` |
|
|
| Headers | Train-Case | `X-Request-Id` |
|
|
|
|
**Python exception:** If your entire stack is Python/snake_case, you MAY use `snake_case` in JSON — but be **consistent across all endpoints**.
|
|
|
|
```
|
|
✅ GET /users ❌ GET /users/
|
|
✅ GET /reports/annual ❌ GET /reports/annual.json
|
|
✅ POST /users ❌ POST /users/create
|
|
```
|
|
|
|
---
|
|
|
|
## 3. HTTP Methods & Status Codes (CRITICAL)
|
|
|
|
### Method Semantics
|
|
|
|
| Method | Semantics | Idempotent | Safe | Request Body |
|
|
|--------|-----------|-----------|------|-------------|
|
|
| GET | Read | ✅ | ✅ | ❌ Never |
|
|
| POST | Create / Action | ❌ | ❌ | ✅ Always |
|
|
| PUT | Full replace | ✅ | ❌ | ✅ Always |
|
|
| PATCH | Partial update | ❌* | ❌ | ✅ Always |
|
|
| DELETE | Remove | ✅ | ❌ | ❌ Rarely |
|
|
|
|
### Status Code Quick Reference
|
|
|
|
**Success:**
|
|
|
|
| Code | When | Response Body |
|
|
|------|------|--------------|
|
|
| 200 OK | GET, PUT, PATCH success | Resource / result |
|
|
| 201 Created | POST created resource | Created resource + `Location` header |
|
|
| 202 Accepted | Async operation started | Job ID / status URL |
|
|
| 204 No Content | DELETE success, PUT with no body | None |
|
|
|
|
**Client Errors:**
|
|
|
|
| Code | When | Key Distinction |
|
|
|------|------|-----------------|
|
|
| 400 Bad Request | Malformed syntax | Can't even parse |
|
|
| 401 Unauthorized | Missing / invalid auth | "Who are you?" |
|
|
| 403 Forbidden | Authenticated, no permission | "I know you, but no" |
|
|
| 404 Not Found | Resource doesn't exist | Also use to hide 403 |
|
|
| 409 Conflict | Duplicate, version mismatch | State conflict |
|
|
| 422 Unprocessable | Valid syntax, failed validation | Semantic errors |
|
|
| 429 Too Many Requests | Rate limit hit | Include `Retry-After` |
|
|
|
|
**Server Errors:** 500 (unexpected), 502 (upstream fail), 503 (overloaded), 504 (upstream timeout)
|
|
|
|
---
|
|
|
|
## 4. Error Handling (HIGH)
|
|
|
|
### Standard Error Envelope (RFC 9457)
|
|
|
|
Every error response uses this format:
|
|
|
|
```json
|
|
{
|
|
"type": "https://api.example.com/errors/insufficient-funds",
|
|
"title": "Insufficient Funds",
|
|
"status": 422,
|
|
"detail": "Account balance $10.00 is less than withdrawal $50.00.",
|
|
"instance": "/transactions/txn_abc123",
|
|
"request_id": "req_7f3a8b2c",
|
|
"errors": [
|
|
{ "field": "amount", "message": "Exceeds balance", "code": "INSUFFICIENT_BALANCE" }
|
|
]
|
|
}
|
|
```
|
|
|
|
### Multi-Language Implementation
|
|
|
|
**TypeScript (Express):**
|
|
```typescript
|
|
class AppError extends Error {
|
|
constructor(
|
|
public readonly title: string,
|
|
public readonly status: number,
|
|
public readonly detail: string,
|
|
public readonly code: string,
|
|
) { super(detail); }
|
|
}
|
|
|
|
// Middleware
|
|
app.use((err, req, res, next) => {
|
|
if (err instanceof AppError) {
|
|
return res.status(err.status).json({
|
|
type: `https://api.example.com/errors/${err.code}`,
|
|
title: err.title, status: err.status,
|
|
detail: err.detail, request_id: req.id,
|
|
});
|
|
}
|
|
res.status(500).json({ title: 'Internal Error', status: 500, request_id: req.id });
|
|
});
|
|
```
|
|
|
|
**Python (FastAPI):**
|
|
```python
|
|
from fastapi import Request
|
|
from fastapi.responses import JSONResponse
|
|
|
|
class AppError(Exception):
|
|
def __init__(self, title: str, status: int, detail: str, code: str):
|
|
self.title, self.status, self.detail, self.code = title, status, detail, code
|
|
|
|
@app.exception_handler(AppError)
|
|
async def app_error_handler(request: Request, exc: AppError):
|
|
return JSONResponse(status_code=exc.status, content={
|
|
"type": f"https://api.example.com/errors/{exc.code}",
|
|
"title": exc.title, "status": exc.status,
|
|
"detail": exc.detail, "request_id": request.state.request_id,
|
|
})
|
|
```
|
|
|
|
### Iron Rules
|
|
|
|
```
|
|
✅ Return RFC 9457 error envelope for ALL errors
|
|
✅ Include request_id in every error response
|
|
✅ Return per-field validation errors in `errors` array
|
|
|
|
❌ Never expose stack traces in production
|
|
❌ Never return 200 for errors
|
|
❌ Never swallow errors silently
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Authentication & Authorization (HIGH)
|
|
|
|
```
|
|
✅ Authorization: Bearer eyJhbGci... (header)
|
|
❌ GET /users?token=eyJhbGci... (URL — appears in logs)
|
|
|
|
✅ 401 → "Who are you?" (missing/invalid credentials)
|
|
✅ 403 → "You can't do this" (authenticated, no permission)
|
|
✅ 404 → Hide resource existence (use instead of 403 when needed)
|
|
```
|
|
|
|
**Rate Limit Headers (always include):**
|
|
```
|
|
X-RateLimit-Limit: 100
|
|
X-RateLimit-Remaining: 42
|
|
X-RateLimit-Reset: 1625097600
|
|
Retry-After: 30
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Pagination & Filtering (HIGH)
|
|
|
|
### Cursor vs Offset
|
|
|
|
| Strategy | When | Pros | Cons |
|
|
|----------|------|------|------|
|
|
| **Cursor** (preferred) | Large/dynamic datasets | Consistent, no skips | Can't jump to page N |
|
|
| **Offset** | Small/stable datasets, admin UIs | Simple, page jumps | Drift on insert/delete |
|
|
|
|
**Cursor pagination response:**
|
|
```json
|
|
{
|
|
"data": [...],
|
|
"pagination": { "next_cursor": "eyJpZCI6MTIwfQ", "has_more": true }
|
|
}
|
|
```
|
|
|
|
**Offset pagination response:**
|
|
```json
|
|
{
|
|
"data": [...],
|
|
"pagination": { "page": 3, "per_page": 20, "total": 256, "total_pages": 13 }
|
|
}
|
|
```
|
|
|
|
**Always enforce:** Default 20 items, max 100 items.
|
|
|
|
### Standard Filter Patterns
|
|
|
|
```
|
|
GET /orders?status=shipped&created_after=2025-01-01&sort=-created_at&fields=id,status
|
|
```
|
|
|
|
| Pattern | Convention |
|
|
|---------|-----------|
|
|
| Exact match | `?status=shipped` |
|
|
| Range | `?price_gte=10&price_lte=100` |
|
|
| Date range | `?created_after=2025-01-01&created_before=2025-12-31` |
|
|
| Sort | `?sort=field` (asc), `?sort=-field` (desc) |
|
|
| Sparse fields | `?fields=id,name,email` |
|
|
| Search | `?q=search+term` |
|
|
|
|
---
|
|
|
|
## 7. Versioning (MEDIUM-HIGH)
|
|
|
|
| Strategy | Format | Best For |
|
|
|----------|--------|----------|
|
|
| **URL path** (recommended) | `/v1/users` | Public APIs |
|
|
| **Header** | `Api-Version: 2` | Internal APIs |
|
|
| **Query param** | `?version=2` | Legacy (avoid) |
|
|
|
|
**Non-breaking changes (no version bump):** New optional response fields, new endpoints, new optional params.
|
|
|
|
**Breaking changes (new version required):** Removing/renaming fields, changing types, stricter validation, removing endpoints.
|
|
|
|
**Deprecation headers:**
|
|
```
|
|
Sunset: Sat, 01 Mar 2026 00:00:00 GMT
|
|
Deprecation: true
|
|
Link: <https://api.example.com/v2/users>; rel="successor-version"
|
|
```
|
|
|
|
---
|
|
|
|
## 8. Request / Response Design (MEDIUM)
|
|
|
|
### Consistent Envelope
|
|
|
|
```json
|
|
{
|
|
"data": { "id": "ord_123", "status": "pending", "total": 99.50 },
|
|
"meta": { "request_id": "req_abc123", "timestamp": "2025-06-15T10:30:00Z" }
|
|
}
|
|
```
|
|
|
|
### Key Rules
|
|
|
|
| Rule | Correct | Wrong |
|
|
|------|---------|-------|
|
|
| Timestamps | `"2025-06-15T10:30:00Z"` (ISO 8601) | `"06/15/2025"` or `1718447400` |
|
|
| Public IDs | UUID `"550e8400-..."` | Auto-increment `42` |
|
|
| Null vs absent (PATCH) | `{ "nickname": null }` = clear field | Absent field = don't change |
|
|
| HATEOAS (public APIs) | `"links": { "cancel": "/orders/123/cancel" }` | No discoverability |
|
|
|
|
---
|
|
|
|
## 9. Documentation — OpenAPI (MEDIUM)
|
|
|
|
**Design-first workflow:**
|
|
|
|
```
|
|
1. Write OpenAPI 3.1 spec
|
|
2. Review spec with stakeholders
|
|
3. Generate server stubs + client SDKs
|
|
4. Implement handlers
|
|
5. Validate responses against spec in CI
|
|
```
|
|
|
|
Every endpoint documents: summary, all parameters, request body + examples, all response codes + schemas, auth requirements.
|
|
|
|
---
|
|
|
|
## 10. API Style Decision Tree
|
|
|
|
```
|
|
What kind of API?
|
|
│
|
|
├─ Browser + mobile clients, flexible queries
|
|
│ └─ GraphQL
|
|
│ Rules: DataLoader (no N+1), depth limit ≤7, Relay pagination
|
|
│
|
|
├─ Standard CRUD, public consumers, caching important
|
|
│ └─ REST (this guide)
|
|
│ Rules: Resources, HTTP methods, status codes, OpenAPI
|
|
│
|
|
├─ Service-to-service, high throughput, strong typing
|
|
│ └─ gRPC
|
|
│ Rules: Protobuf schemas, streaming for large data, deadlines
|
|
│
|
|
├─ Full-stack TypeScript, same team owns client + server
|
|
│ └─ tRPC
|
|
│ Rules: Shared types, no code generation needed
|
|
│
|
|
└─ Real-time bidirectional
|
|
└─ WebSocket / SSE
|
|
Rules: Heartbeat, reconnection, message ordering
|
|
```
|
|
|
|
---
|
|
|
|
## Anti-Patterns Checklist
|
|
|
|
| # | ❌ Don't | ✅ Do Instead |
|
|
|---|---------|--------------|
|
|
| 1 | Verbs in URLs (`/getUser`) | HTTP methods + noun resources |
|
|
| 2 | Return 200 for errors | Correct 4xx/5xx status codes |
|
|
| 3 | Mix naming styles | One convention per context |
|
|
| 4 | Expose database IDs | UUIDs for public identifiers |
|
|
| 5 | No pagination on lists | Always paginate (default 20) |
|
|
| 6 | Swallow errors silently | Structured RFC 9457 errors |
|
|
| 7 | Token in URL query | Authorization header |
|
|
| 8 | Deep nesting (3+ levels) | Flatten with query params |
|
|
| 9 | Break changes without version | Maintain compatibility or version |
|
|
| 10 | No rate limiting | Implement + communicate via headers |
|
|
| 11 | No request ID | `X-Request-Id` on every response |
|
|
| 12 | Stack traces in production | Safe error message + internal log |
|
|
|
|
---
|
|
|
|
## Common Issues
|
|
|
|
### Issue 1: "Should this be a new resource or a sub-resource?"
|
|
|
|
**Symptom:** URL path keeps growing (`/users/{id}/orders/{id}/items/{id}/reviews`)
|
|
|
|
**Rule:** If the child entity makes sense on its own, promote it. If it only exists within the parent context, keep it nested (max 2 levels).
|
|
|
|
```
|
|
/reviews?orderId=123 ✅ (reviews exist independently)
|
|
/orders/{id}/items ✅ (items belong to orders, 1 level)
|
|
```
|
|
|
|
### Issue 2: "PUT or PATCH?"
|
|
|
|
**Symptom:** Team can't agree on update semantics.
|
|
|
|
**Rule:**
|
|
- PUT = client sends **complete** resource (missing fields → set to default/null)
|
|
- PATCH = client sends **only changed fields** (missing fields → unchanged)
|
|
- When unsure → **PATCH** (safer, less surprising)
|
|
|
|
### Issue 3: "400 or 422?"
|
|
|
|
**Symptom:** Inconsistent validation error codes.
|
|
|
|
**Rule:**
|
|
- 400 = can't parse request at all (malformed JSON, wrong content-type)
|
|
- 422 = parsed OK, but values fail validation (invalid email, negative quantity)
|