Files
skills/fullstack-dev/references/api-design.md
shihao 6487becf60 Initial commit: add all skills files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:52:49 +08:00

14 KiB

name, description, license, metadata
name description license metadata
fullstack-dev-api-design 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. MIT
version sources
2.0.0
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
Pick HTTP method + status code 3. HTTP Methods & Status Codes
Format error responses 4. Error Handling
Add pagination or filtering 6. Pagination & Filtering
Choose API style (REST vs GraphQL vs gRPC) 10. API Style Decision
Version an existing API 7. Versioning
Avoid common mistakes Anti-Patterns

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:

{
  "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):

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):

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:

{
  "data": [...],
  "pagination": { "next_cursor": "eyJpZCI6MTIwfQ", "has_more": true }
}

Offset pagination response:

{
  "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

{
  "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)