Initial commit: add all skills files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
444
fullstack-dev/references/api-design.md
Normal file
444
fullstack-dev/references/api-design.md
Normal file
@@ -0,0 +1,444 @@
|
||||
---
|
||||
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)
|
||||
Reference in New Issue
Block a user