Backend APIs: HTTP Service Design and Implementation
Design and review HTTP APIs that stay coherent as they grow. Focus on contracts, auth
boundaries, error models, and framework structure for Python and Node.js services.
Reviewing an existing FastAPI, Express, or NestJS service
Writing or fixing OpenAPI specifications and generated docs
Choosing status codes, error formats, pagination, filtering, or versioning strategy
Designing auth flows for browser, mobile, machine-to-machine, or third-party clients
Refactoring route or controller structure that has become hard to reason about
Planning backward-compatible API evolution
Defining idempotency and retry behavior for write endpoints
When NOT to use
GraphQL schema, resolvers, federation, or persisted query work - out of scope for this skill
gRPC, protobuf schema evolution, or streaming RPCs - out of scope for this skill
Database schema, indexing, replication, or query tuning - use databases
General bug-finding, race-condition hunting, or correctness review outside the API boundary - use code-review
Security auditing for auth bypasses, injection, secrets, or OWASP findings - use security-audit
Writing or debugging automated tests - use testing
Deployment, containers, gateways, ingress, or cluster config - use docker, kubernetes, or networking
CI/CD pipeline design for API delivery - use ci-cd
MCP-specific HTTP servers and tool handlers - use mcp
AI Self-Check
Before returning API code, route design, or OpenAPI output, verify:
Resource names are nouns and URL shape is stable (/users/{userId}/sessions, not /doUserSessionThing)
Method semantics follow RFC 9110 - no "GET that mutates", no PATCH used as a vague catch-all
401 vs 403 is correct: unauthenticated vs authenticated-but-forbidden
Ownership-hiding behavior is deliberate and consistent: decide when out-of-scope resources return 404 vs 403
Error responses use one consistent format, preferably RFC 9457 problem details
Request and response DTOs are explicit and separate from ORM or database models
Input validation happens server-side even if clients or SDKs also validate
Offset pagination is not used blindly on large or high-churn collections where cursor pagination is the safer default
Write endpoints that may be retried (POST /payments, webhook receivers, order creation) define idempotency behavior
OpenAPI docs match real handler behavior, examples, status codes, and auth requirements
First-party browser apps do not get long-lived bearer tokens stored in browser storage by default
Cookie-based browser auth accounts for cookie scope and CSRF behavior instead of assuming cookies are automatically safe
OAuth guidance is current: authorization code + PKCE, no implicit flow, no resource owner password credentials
Sensitive defaults are explicit: cookie flags, token TTLs, scope boundaries, and rate limits are not hand-waved
Current source checked: dated versions, CLI flags, API names, and support windows are verified against primary docs before repeating them
Hidden state identified: local config, credentials, caches, contexts, branches, cluster targets, or previous runs are made explicit before acting
Verification is real: final checks exercise the actual runtime, parser, service, or integration point instead of only linting prose or happy paths
Routing overlap checked: overlapping skills, trigger terms, and "When NOT to use" boundaries are checked before returning guidance
Spec claims verified: claims about tool behavior, output contracts, or repo conventions are checked against current docs, scripts, or skill files
Framework version checked: FastAPI, Express, NestJS, and OpenAPI examples match current APIs and migration notes
Contract compatibility checked: response codes, pagination, errors, and auth behavior preserve existing clients unless a version bump is explicit
Injection in handler body flagged as contract defect: if injection (SQL, command, header, path traversal) is found inside a route handler, flag it as a missing validation-boundary defect at the contract layer and recommend fixing it there; route to security-audit for a full injection audit if the pattern is widespread
Performance
Paginate and filter at the data store; never load full collections just to slice in the handler.
Set timeouts and body limits at the framework, proxy, and client layers.
Measure p95/p99 latency and error rates for changed endpoints before optimizing internals.
Best Practices
Design idempotency for retries on create, payment, provisioning, and webhook endpoints.
Generate or validate OpenAPI from the implementation contract and keep examples executable.
Use explicit auth scopes and tenancy checks in handlers, not only in route grouping or UI state.
Workflow
Build vs. Review: when reviewing an existing service, still walk the same steps. Determine the API boundary, audit the contract, trace auth, then compare the implementation against the published behavior instead of jumping straight into handler code.
Step 1: Determine the boundary
Clarify the API before picking framework patterns:
Who calls it? Browser app, mobile app, third-party integrator, internal service, webhook sender
What kind of API is it? Public API, private app backend, internal service, admin API
Is the task greenfield or review? New design, incremental change, migration, or bug fix
What stability promise exists? Internal-only, versioned public API, or "best effort"
What auth model already exists? Session cookies, JWT bearer, API keys, OAuth/OIDC, or none yet
What house style already exists? Error format, pagination shape, versioning scheme, auth boundary, DTO naming
If the user asks to "just add an endpoint", inspect the surrounding service first. Most bad API
work happens when one route lands with a different error model, auth rule, pagination shape, or
DTO style than the rest of the service.
Step 2: Choose the framework pattern
Pick the framework that matches the codebase and team constraints. Do not switch frameworks for fashion.
Framework
Best fit
Watch for
FastAPI
Python services with typed request/response models and strong OpenAPI output
Leaking ORM models directly, async confusion, piling business logic into route functions
No built-in structure, validation scattered across middleware, inconsistent error handling
NestJS
Larger TypeScript services that benefit from modules, guards, pipes, interceptors, and DI
Over-abstraction, decorator-heavy indirection, hiding simple flows behind too many layers
Framework rules:
FastAPI - keep dependencies explicit, use response models, and centralize exception handling
Express - keep routing, validation, auth, and business logic separated; add one error middleware path
NestJS - keep controllers thin, move auth into guards, validation into pipes, and cross-cutting behavior into interceptors or filters
Step 3: Design the contract first
Define the API contract before writing handlers:
For changes to an existing service, match the current URL, error, pagination, and auth house
style unless the task explicitly includes a migration.
Pick resource shapes and URIs
Choose methods and status codes
Define request and response schemas
Define error shapes
Decide pagination, filtering, sorting, and versioning rules
Only then wire the framework implementation
Prefer:
Noun-based resources: /users/{userId}/sessions
Predictable list endpoints: GET /orders?cursor=...&limit=...&status=paid
Explicit write semantics: POST for create/commands, PUT for full replacement, PATCH for partial update, DELETE for delete
Explicit retry semantics for command endpoints like POST /orders/{orderId}/cancel
One canonical error model across the whole service
Read references/http-api-patterns.md for method semantics, pagination, idempotency, and versioning rules.
Step 4: Design auth around the client, not around JWT hype
Start from the client type:
Client
Default choice
Why
First-party browser app
Session cookie or BFF/token-mediating backend
Keeps tokens off the browser as much as possible
Native/mobile app
OAuth/OIDC authorization code + PKCE
Current standard path for public clients
Third-party integrator
OAuth/OIDC or scoped API keys if OAuth is overkill
Explicit delegation and revocation story
Internal service-to-service
Platform identity or short-lived service credentials
Avoid user-style auth flows between services
Auth rules:
Use sessions by default for first-party web apps unless there is a clear reason not to
Use bearer tokens when the client really is a token-holding client
Treat API keys as machine credentials, not as a universal auth shortcut
Define scope or role boundaries at the API boundary, not ad hoc inside random handlers
If the browser app talks to third-party identity providers, follow authorization code + PKCE and current browser-app guidance rather than resurrecting implicit flow patterns
BFF token mediation (first-party browser app calling an external API):
Offset pagination is fine for small, stable backoffice lists
Cursor pagination is the default for user-facing or high-write collections
Sort order must be stable and documented
Filter names should reflect resource fields or clear domain concepts, not internal SQL column names
Idempotency and retries
Define idempotency for writes that clients or gateways may retry
Use explicit idempotency keys for payment-like or request-replay-prone operations
Distinguish "request accepted" from "side effect completed" when async workflows exist
Idempotency key header pattern (Express/NestJS):
const key = req.headers['idempotency-key'];
if (key) {
const cached = await cache.get(`idem:${key}`);
if (cached) return res.status(cached.status).json(cached.body);
}
// ... execute, then store result keyed by idempotency-key before returning
Idempotency key header pattern (FastAPI):
from fastapi import Header, HTTPException
async def create_order(
body: OrderCreate,
idempotency_key: str | None = Header(default=None, alias="Idempotency-Key"),
):
if idempotency_key:
cached = await cache.get(f"idem:{idempotency_key}")
if cached:
return JSONResponse(status_code=cached["status"], content=cached["body"])
result = await orders.create(body)
if idempotency_key:
await cache.set(f"idem:{idempotency_key}", {"status": 201, "body": result}, ttl=86400)
return result
Decode the cursor server-side (WHERE id > :cursor_id ORDER BY id LIMIT :limit). Never expose raw DB offsets or row numbers in the cursor.
Opaque cursor encode/decode (FastAPI, HMAC-signed to prevent client tampering):
import base64, hmac, hashlib, json, os
SECRET = os.environ["CURSOR_SECRET"].encode()
def encode_cursor(payload: dict) -> str:
body = base64.urlsafe_b64encode(json.dumps(payload, separators=(",", ":")).encode()).rstrip(b"=")
sig = base64.urlsafe_b64encode(hmac.new(SECRET, body, hashlib.sha256).digest()[:8]).rstrip(b"=")
return f"{body.decode()}.{sig.decode()}"
def decode_cursor(token: str) -> dict:
body, sig = token.split(".", 1)
expected = base64.urlsafe_b64encode(hmac.new(SECRET, body.encode(), hashlib.sha256).digest()[:8]).rstrip(b"=").decode()
if not hmac.compare_digest(sig, expected):
raise ValueError("invalid cursor")
return json.loads(base64.urlsafe_b64decode(body + "=="))
Clients treat next_cursor as opaque; the server controls shape and can change it without a breaking contract.
Graceful shutdown and rolling deploys
Rolling deploys send SIGTERM to old instances while new ones come up. Handle it on the API side or expect 502s under load:
Trap SIGTERM, stop accepting new connections, drain in-flight requests, then exit. FastAPI/Uvicorn: configure --timeout-graceful-shutdown (default 30s); Express 5: server.close() then server.closeAllConnections() after the drain window; NestJS: app.enableShutdownHooks() plus onApplicationShutdown handlers
Keep the app-side shutdown window shorter than the orchestrator's termination grace (Kubernetes default 30s). app_shutdown < terminationGracePeriodSeconds or the kernel kills in-flight work
Set HTTP keep-alive timeout shorter than any upstream idle timeout (load balancer, ingress). If the LB holds a connection the server already closed, the next request hits a dead socket. Typical safe pair: server keep-alive 65s behind an LB with 60s idle
Add a readiness probe that flips to failing on SIGTERM before the drain starts. The orchestrator stops routing new traffic while in-flight requests finish
What NOT to Force
Do not force cursor pagination onto every small internal list or backoffice table
Do not cut /v2 just because a new field got added
Do not insist on OAuth for every internal automation path if scoped API keys or platform identity fit better
Do not push first-party browser apps toward bearer-token-in-local-storage patterns because "JWT auth" sounds modern
Do not turn simple Express services into pseudo-enterprise architectures with needless layers and decorators
See skills/_shared/output-contract.md for the full contract.
Skill name: BACKEND-API
Deliverable bucket:audits
Mode: conditional. When invoked to analyze, review, audit, or improve existing repo content, emit the full contract - boxed inline header, body summary inline plus per-finding detail in the deliverable file, boxed conclusion, conclusion table - and write the deliverable to docs/local/audits/backend-api/<YYYY-MM-DD>-<slug>.md. When invoked to answer a question, teach a concept, build a new artifact, or generate content, respond freely without the contract.
Severity scale:P0 | P1 | P2 | P3 | info (see shared contract; only used in audit/review mode).
Related Skills
databases - schema design, query tuning, migrations, and persistence concerns behind the API
code-review - correctness bugs, logic errors, and non-API-specific review findings
security-audit - auth bypasses, OWASP findings, secrets, and security review after the API design exists
testing - route, integration, contract, and end-to-end verification
Contract before handlers. Design the resource model, schemas, errors, and auth boundary before writing route code.
Do not let transport leak persistence. Database tables, ORM entities, and internal enums are not public API contracts.
Pick one error format. Prefer RFC 9457 and apply it consistently.
Use current auth guidance. Authorization code + PKCE for public OAuth clients, sessions or BFF patterns for first-party browser apps, no implicit flow, no password grant.
Keep framework structure boring. Thin controllers or routes, explicit validation, explicit auth, centralized error handling.
Backward compatibility is a feature. New fields are cheap; breaking clients is expensive.
Write for real retries. Assume clients, proxies, and job runners will replay requests.
Keep scope on HTTP APIs. When the problem is GraphQL or gRPC-specific, say so and stop pretending the same rules apply.
Match service conventions unless migrating them. A one-off endpoint with a different error, auth, or pagination model is usually a contract bug.
1---2name: backend-api-33description: · Design/review HTTP APIs for FastAPI, Express, NestJS: REST, OpenAPI, pagination, OAuth/JWT. Triggers: 'fastapi', 'express', 'nestjs', 'openapi', 'pagination', 'idempotency', 'rest api', 'endpoint'. Not for schemas (use databases).4license: MIT5---67# Backend APIs: HTTP Service Design and Implementation
89Design and review HTTP APIs that stay coherent as they grow. Focus on contracts, auth
10boundaries, error models, and framework structure for Python and Node.js services.
1112**Target versions** (May 2026):
13- FastAPI **0.135.3** (released 2026-04-01)
14- Express **5.2.1** (published 2025-12-01)
15- NestJS **11.1.18** (published 2026-04-03)
16- OpenAPI Specification **3.2.0** (published 2025-09-19)
17- HTTP Semantics: **RFC 9110** (June 2022)
18- Problem Details for HTTP APIs: **RFC 9457** (July 2023)
19- OAuth 2.0 Security Best Current Practice: **RFC 9700** (January 2025)
2021This skill works across five concerns:
22- **Contract design** - resources, methods, status codes, schemas, versioning
23- **API ergonomics** - pagination, filtering, sorting, idempotency, error format
24- **Framework structure** - FastAPI dependencies, Express middleware, NestJS modules and guards
25- **Authentication** - sessions, bearer tokens, OAuth/OIDC, BFF and token mediation
26- **Review** - unstable contracts, DTO leakage, auth confusion, and HTTP misuse
2728## When to use
2930- Designing a new REST or HTTP API
31- Reviewing an existing FastAPI, Express, or NestJS service
32- Writing or fixing OpenAPI specifications and generated docs
33- Choosing status codes, error formats, pagination, filtering, or versioning strategy
34- Designing auth flows for browser, mobile, machine-to-machine, or third-party clients
35- Refactoring route or controller structure that has become hard to reason about
36- Planning backward-compatible API evolution
37- Defining idempotency and retry behavior for write endpoints
3839## When NOT to use
4041- GraphQL schema, resolvers, federation, or persisted query work - out of scope for this skill
42- gRPC, protobuf schema evolution, or streaming RPCs - out of scope for this skill
43- Database schema, indexing, replication, or query tuning - use **databases**
44- General bug-finding, race-condition hunting, or correctness review outside the API boundary - use **code-review**
45- Security auditing for auth bypasses, injection, secrets, or OWASP findings - use **security-audit**
46- Writing or debugging automated tests - use **testing**
47- Deployment, containers, gateways, ingress, or cluster config - use **docker**, **kubernetes**, or **networking**
48- CI/CD pipeline design for API delivery - use **ci-cd**
49- MCP-specific HTTP servers and tool handlers - use **mcp**
5051---
5253## AI Self-Check
5455Before returning API code, route design, or OpenAPI output, verify:
5657- [ ] Resource names are nouns and URL shape is stable (`/users/{userId}/sessions`, not `/doUserSessionThing`)
58- [ ] Method semantics follow RFC 9110 - no "GET that mutates", no PATCH used as a vague catch-all
59- [ ] `401` vs `403` is correct: unauthenticated vs authenticated-but-forbidden
60- [ ] Ownership-hiding behavior is deliberate and consistent: decide when out-of-scope resources return `404` vs `403`
61- [ ] Error responses use one consistent format, preferably RFC 9457 problem details
62- [ ] Request and response DTOs are explicit and separate from ORM or database models
63- [ ] Input validation happens server-side even if clients or SDKs also validate
64- [ ] Offset pagination is not used blindly on large or high-churn collections where cursor pagination is the safer default
65- [ ] Write endpoints that may be retried (`POST /payments`, webhook receivers, order creation) define idempotency behavior
66- [ ] OpenAPI docs match real handler behavior, examples, status codes, and auth requirements
67- [ ] First-party browser apps do not get long-lived bearer tokens stored in browser storage by default
68- [ ] Cookie-based browser auth accounts for cookie scope and CSRF behavior instead of assuming cookies are automatically safe
69- [ ] OAuth guidance is current: authorization code + PKCE, no implicit flow, no resource owner password credentials
70- [ ] Sensitive defaults are explicit: cookie flags, token TTLs, scope boundaries, and rate limits are not hand-waved
71- [ ] **Current source checked**: dated versions, CLI flags, API names, and support windows are verified against primary docs before repeating them
72- [ ] **Hidden state identified**: local config, credentials, caches, contexts, branches, cluster targets, or previous runs are made explicit before acting
73- [ ] **Verification is real**: final checks exercise the actual runtime, parser, service, or integration point instead of only linting prose or happy paths
74- [ ] **Routing overlap checked**: overlapping skills, trigger terms, and "When NOT to use" boundaries are checked before returning guidance
75- [ ] **Spec claims verified**: claims about tool behavior, output contracts, or repo conventions are checked against current docs, scripts, or skill files
76- [ ] **Framework version checked**: FastAPI, Express, NestJS, and OpenAPI examples match current APIs and migration notes
77- [ ] **Contract compatibility checked**: response codes, pagination, errors, and auth behavior preserve existing clients unless a version bump is explicit
78- [ ] **Injection in handler body flagged as contract defect**: if injection (SQL, command, header, path traversal) is found inside a route handler, flag it as a missing validation-boundary defect at the contract layer and recommend fixing it there; route to **security-audit** for a full injection audit if the pattern is widespread
7980---
8182## Performance
8384- Paginate and filter at the data store; never load full collections just to slice in the handler.
85- Set timeouts and body limits at the framework, proxy, and client layers.
86- Measure p95/p99 latency and error rates for changed endpoints before optimizing internals.
878889---
9091## Best Practices
9293- Design idempotency for retries on create, payment, provisioning, and webhook endpoints.
94- Generate or validate OpenAPI from the implementation contract and keep examples executable.
95- Use explicit auth scopes and tenancy checks in handlers, not only in route grouping or UI state.
969798## Workflow
99100**Build vs. Review:** when reviewing an existing service, still walk the same steps. Determine the API boundary, audit the contract, trace auth, then compare the implementation against the published behavior instead of jumping straight into handler code.
101102### Step 1: Determine the boundary
103104Clarify the API before picking framework patterns:
105- **Who calls it?** Browser app, mobile app, third-party integrator, internal service, webhook sender
106- **What kind of API is it?** Public API, private app backend, internal service, admin API
107- **Is the task greenfield or review?** New design, incremental change, migration, or bug fix
108- **What stability promise exists?** Internal-only, versioned public API, or "best effort"
109- **What auth model already exists?** Session cookies, JWT bearer, API keys, OAuth/OIDC, or none yet
110- **What house style already exists?** Error format, pagination shape, versioning scheme, auth boundary, DTO naming
111112If the user asks to "just add an endpoint", inspect the surrounding service first. Most bad API
113work happens when one route lands with a different error model, auth rule, pagination shape, or
114DTO style than the rest of the service.
115116### Step 2: Choose the framework pattern
117118Pick the framework that matches the codebase and team constraints. Do not switch frameworks for fashion.
119120| Framework | Best fit | Watch for |
121|-----------|----------|-----------|
122| **FastAPI** | Python services with typed request/response models and strong OpenAPI output | Leaking ORM models directly, async confusion, piling business logic into route functions |
123| **Express** | Thin Node.js services, custom middleware stacks, existing mature codebases | No built-in structure, validation scattered across middleware, inconsistent error handling |
124| **NestJS** | Larger TypeScript services that benefit from modules, guards, pipes, interceptors, and DI | Over-abstraction, decorator-heavy indirection, hiding simple flows behind too many layers |
125126Framework rules:
127- **FastAPI** - keep dependencies explicit, use response models, and centralize exception handling
128- **Express** - keep routing, validation, auth, and business logic separated; add one error middleware path
129- **NestJS** - keep controllers thin, move auth into guards, validation into pipes, and cross-cutting behavior into interceptors or filters
130131### Step 3: Design the contract first
132133Define the API contract before writing handlers:
134135For changes to an existing service, match the current URL, error, pagination, and auth house
136style unless the task explicitly includes a migration.
1371381. Pick resource shapes and URIs
1392. Choose methods and status codes
1403. Define request and response schemas
1414. Define error shapes
1425. Decide pagination, filtering, sorting, and versioning rules
1436. Only then wire the framework implementation
144145Prefer:
146- Noun-based resources: `/users/{userId}/sessions`
147- Predictable list endpoints: `GET /orders?cursor=...&limit=...&status=paid`
148- Explicit write semantics: `POST` for create/commands, `PUT` for full replacement, `PATCH` for partial update, `DELETE` for delete
149- Explicit retry semantics for command endpoints like `POST /orders/{orderId}/cancel`
150- One canonical error model across the whole service
151152Read `references/http-api-patterns.md` for method semantics, pagination, idempotency, and versioning rules.
153154### Step 4: Design auth around the client, not around JWT hype
155156Start from the client type:
157158| Client | Default choice | Why |
159|--------|----------------|-----|
160| **First-party browser app** | Session cookie or BFF/token-mediating backend | Keeps tokens off the browser as much as possible |
161| **Native/mobile app** | OAuth/OIDC authorization code + PKCE | Current standard path for public clients |
162| **Third-party integrator** | OAuth/OIDC or scoped API keys if OAuth is overkill | Explicit delegation and revocation story |
163| **Internal service-to-service** | Platform identity or short-lived service credentials | Avoid user-style auth flows between services |
164165Auth rules:
166- Use sessions by default for first-party web apps unless there is a clear reason not to
167- Use bearer tokens when the client really is a token-holding client
168- Treat API keys as machine credentials, not as a universal auth shortcut
169- Define scope or role boundaries at the API boundary, not ad hoc inside random handlers
170- If the browser app talks to third-party identity providers, follow authorization code + PKCE and current browser-app guidance rather than resurrecting implicit flow patterns
171172BFF token mediation (first-party browser app calling an external API):
173```
174Browser -> BFF (session cookie) -> BFF attaches Bearer token -> Upstream API
175```
176The BFF holds the access token server-side; the browser never sees it.
177178Read `references/auth-and-session-patterns.md` for sessions, bearer tokens, OAuth, BFF, refresh tokens, and machine auth.
179180### Step 5: Implement the framework surface
181182Convert the contract into framework code without letting the framework dictate the contract.
183184**FastAPI**
185- Define Pydantic models for requests and responses
186- Use dependency injection for auth, DB/session acquisition, and shared request context
187- Register exception handlers for consistent RFC 9457 output
188- Keep route handlers thin; move business rules into services or domain modules
189190**Express**
191- Validate inputs before controller logic
192- Attach auth and request context in middleware
193- Use one shared error-handling middleware path
194- Keep controllers as transport adapters, not the place where every rule in the system lives
195196**NestJS**
197- Use DTO classes for transport boundaries
198- Put validation in pipes and auth in guards
199- Use exception filters or interceptors to normalize response and error behavior
200- Keep module boundaries meaningful; one module per vague concept is not architecture
201202### Step 6: Validate behavior and docs
203204Before returning the result:
205- Compare OpenAPI docs against the real routes
206- Verify auth requirements per endpoint
207- Check all non-2xx responses, not just the happy path
208- Verify list endpoints under empty, partial, and end-of-cursor states
209- Check retries and duplicate submissions for create/payment/webhook paths
210- Check repeat-call behavior for command endpoints (`cancel`, `resend`, `approve`) instead of leaving retries implicit
211- Compare new endpoints with neighboring endpoints for naming, DTO shape, pagination params, and error consistency
212- Route follow-up testing work to **testing** and follow-up security review to **security-audit**
213214---
215216## Contract Guardrails
217218### Versioning
219220- Prefer additive change over version churn
221- Version only when you need a breaking change
222- Keep one versioning scheme for the service: path versioning (`/v1/...`) is the clearest default for public APIs
223- Do not mix path versioning, header versioning, and ad hoc `?version=` query params in one API
224225### OpenAPI authoring
226227- OpenAPI `3.2.0` is the current spec, but much of the framework and Swagger ecosystem still centers on `3.1.x`
228- Default to authoring for `3.1` compatibility unless the actual toolchain in the project proves `3.2` support end-to-end
229- Do not advertise `3.2` features in generated specs just because the top-level standard moved
230231### Error model
232233- Prefer RFC 9457 problem details with stable `type`, `title`, `status`, and domain-specific extension fields
234- Do not return one error shape from validation, another from auth, and a third from business rules
235- Never leak raw stack traces or ORM internals to clients
236237Minimal RFC 9457 problem detail response:
238```json
239{
240 "type": "https://api.example.com/errors/insufficient-funds",
241 "title": "Insufficient funds",
242 "status": 422,
243 "detail": "Account balance is $10.00; transfer requires $50.00."
244}
245```
246247Status code decision matrix (pick the narrowest correct code):
248249| Scenario | Code | Notes |
250|----------|------|-------|
251| Malformed JSON, missing required field, wrong type | `400` | Client request is syntactically wrong |
252| Well-formed input but fails domain rule (insufficient funds, invalid state) | `422` | Semantic validation |
253| No credentials, expired token | `401` | Include `WWW-Authenticate` header |
254| Authenticated but not permitted | `403` | Do not challenge for credentials |
255| Resource does not exist, or exists but caller must not know | `404` | Pick one policy per resource and keep it |
256| Write conflicts with current state (stale ETag, duplicate unique key) | `409` | Problem detail should name the conflict |
257| Idempotency-Key reused with a different body | `422` | Not `409` - the key contract is broken |
258| Too many requests | `429` | Include `Retry-After` |
259| Unhandled server error | `500` | Never leak stack traces in the body |
260261FastAPI exception handler wiring for consistent problem+json:
262```python
263from fastapi import FastAPI, Request
264from fastapi.responses import JSONResponse
265from fastapi.exceptions import RequestValidationError
266267app = FastAPI()
268269@app.exception_handler(RequestValidationError)
270async def validation_handler(request: Request, exc: RequestValidationError):
271 return JSONResponse(
272 status_code=400,
273 media_type="application/problem+json",
274 content={
275 "type": "https://api.example.com/errors/validation",
276 "title": "Invalid request",
277 "status": 400,
278 "errors": exc.errors(),
279 },
280 )
281```
282283### Pagination and filtering
284285- Offset pagination is fine for small, stable backoffice lists
286- Cursor pagination is the default for user-facing or high-write collections
287- Sort order must be stable and documented
288- Filter names should reflect resource fields or clear domain concepts, not internal SQL column names
289290### Idempotency and retries
291292- Define idempotency for writes that clients or gateways may retry
293- Use explicit idempotency keys for payment-like or request-replay-prone operations
294- Distinguish "request accepted" from "side effect completed" when async workflows exist
295296Idempotency key header pattern (Express/NestJS):
297```typescript
298const key = req.headers['idempotency-key'];
299if (key) {
300 const cached = await cache.get(`idem:${key}`);
301 if (cached) return res.status(cached.status).json(cached.body);
302}
303// ... execute, then store result keyed by idempotency-key before returning
304```
305306Idempotency key header pattern (FastAPI):
307```python
308from fastapi import Header, HTTPException
309310async def create_order(
311 body: OrderCreate,
312 idempotency_key: str | None = Header(default=None, alias="Idempotency-Key"),
313):
314 if idempotency_key:
315 cached = await cache.get(f"idem:{idempotency_key}")
316 if cached:
317 return JSONResponse(status_code=cached["status"], content=cached["body"])
318 result = await orders.create(body)
319 if idempotency_key:
320 await cache.set(f"idem:{idempotency_key}", {"status": 201, "body": result}, ttl=86400)
321 return result
322```
323324Cursor pagination response envelope:
325```json
326{
327 "data": [...],
328 "next_cursor": "eyJpZCI6MTIzfQ",
329 "has_more": true
330}
331```
332Decode the cursor server-side (`WHERE id > :cursor_id ORDER BY id LIMIT :limit`). Never expose raw DB offsets or row numbers in the cursor.
333334Opaque cursor encode/decode (FastAPI, HMAC-signed to prevent client tampering):
335```python
336import base64, hmac, hashlib, json, os
337338SECRET = os.environ["CURSOR_SECRET"].encode()
339340def encode_cursor(payload: dict) -> str:
341 body = base64.urlsafe_b64encode(json.dumps(payload, separators=(",", ":")).encode()).rstrip(b"=")
342 sig = base64.urlsafe_b64encode(hmac.new(SECRET, body, hashlib.sha256).digest()[:8]).rstrip(b"=")
343 return f"{body.decode()}.{sig.decode()}"
344345def decode_cursor(token: str) -> dict:
346 body, sig = token.split(".", 1)
347 expected = base64.urlsafe_b64encode(hmac.new(SECRET, body.encode(), hashlib.sha256).digest()[:8]).rstrip(b"=").decode()
348 if not hmac.compare_digest(sig, expected):
349 raise ValueError("invalid cursor")
350 return json.loads(base64.urlsafe_b64decode(body + "=="))
351```
352Clients treat `next_cursor` as opaque; the server controls shape and can change it without a breaking contract.
353354### Graceful shutdown and rolling deploys
355356Rolling deploys send SIGTERM to old instances while new ones come up. Handle it on the API side or expect 502s under load:
357358- Trap SIGTERM, stop accepting new connections, drain in-flight requests, then exit. FastAPI/Uvicorn: configure `--timeout-graceful-shutdown` (default 30s); Express 5: `server.close()` then `server.closeAllConnections()` after the drain window; NestJS: `app.enableShutdownHooks()` plus `onApplicationShutdown` handlers
359- Keep the app-side shutdown window shorter than the orchestrator's termination grace (Kubernetes default 30s). `app_shutdown < terminationGracePeriodSeconds` or the kernel kills in-flight work
360- Set HTTP keep-alive timeout shorter than any upstream idle timeout (load balancer, ingress). If the LB holds a connection the server already closed, the next request hits a dead socket. Typical safe pair: server keep-alive 65s behind an LB with 60s idle
361- Add a readiness probe that flips to failing on SIGTERM before the drain starts. The orchestrator stops routing new traffic while in-flight requests finish
362363## What NOT to Force
364365- Do not force cursor pagination onto every small internal list or backoffice table
366- Do not cut `/v2` just because a new field got added
367- Do not insist on OAuth for every internal automation path if scoped API keys or platform identity fit better
368- Do not push first-party browser apps toward bearer-token-in-local-storage patterns because "JWT auth" sounds modern
369- Do not turn simple Express services into pseudo-enterprise architectures with needless layers and decorators
370371---
372373## Reference Files
374375- `references/http-api-patterns.md` - resource design, method semantics, status codes, versioning, pagination, filtering, idempotency
376- `references/auth-and-session-patterns.md` - sessions vs bearer tokens, OAuth/OIDC, BFF, refresh tokens, machine clients
377378## Output Contract
379380See `skills/_shared/output-contract.md` for the full contract.
381382- **Skill name:** BACKEND-API
383- **Deliverable bucket:** `audits`
384- **Mode:** conditional. When invoked to **analyze, review, audit, or improve** existing repo content, emit the full contract - boxed inline header, body summary inline plus per-finding detail in the deliverable file, boxed conclusion, conclusion table - and write the deliverable to `docs/local/audits/backend-api/<YYYY-MM-DD>-<slug>.md`. When invoked to **answer a question, teach a concept, build a new artifact, or generate content**, respond freely without the contract.
385- **Severity scale:** `P0 | P1 | P2 | P3 | info` (see shared contract; only used in audit/review mode).
386387## Related Skills
388389- **databases** - schema design, query tuning, migrations, and persistence concerns behind the API
390- **code-review** - correctness bugs, logic errors, and non-API-specific review findings
391- **security-audit** - auth bypasses, OWASP findings, secrets, and security review after the API design exists
392- **testing** - route, integration, contract, and end-to-end verification
393- **docker** - containerizing API services
394- **kubernetes** - deploying API services on clusters
395- **networking** - reverse proxies, TLS termination, CORS-adjacent network boundaries, and gateway behavior
396- **ci-cd** - delivery pipelines for API services
397- **mcp** - MCP-specific HTTP servers and auth patterns
398399## Rules
4004011. **Contract before handlers.** Design the resource model, schemas, errors, and auth boundary before writing route code.
4022. **Do not let transport leak persistence.** Database tables, ORM entities, and internal enums are not public API contracts.
4033. **Pick one error format.** Prefer RFC 9457 and apply it consistently.
4044. **Use current auth guidance.** Authorization code + PKCE for public OAuth clients, sessions or BFF patterns for first-party browser apps, no implicit flow, no password grant.
4055. **Keep framework structure boring.** Thin controllers or routes, explicit validation, explicit auth, centralized error handling.
4066. **Backward compatibility is a feature.** New fields are cheap; breaking clients is expensive.
4077. **Write for real retries.** Assume clients, proxies, and job runners will replay requests.
4088. **Keep scope on HTTP APIs.** When the problem is GraphQL or gRPC-specific, say so and stop pretending the same rules apply.
4099. **Match service conventions unless migrating them.** A one-off endpoint with a different error, auth, or pagination model is usually a contract bug.
Run npx skillmds add majiayu000/backend-api-3 in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
· Design/review HTTP APIs for FastAPI, Express, NestJS: REST, OpenAPI, pagination, OAuth/JWT. Triggers: 'fastapi', 'express', 'nestjs', 'openapi', 'pagination', 'idempotency', 'rest api', 'endpoint'. Not for schemas (use databases). It is listed under Integrations & APIs on SkillMD.
This skill has not completed SkillMD's automated safety review yet. Capability flags: makes network calls, reads secrets. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free. This skill is licensed under MIT.
majiayu000 (@majiayu000) published this skill. Their other Agent Skills are listed on their SkillMD profile.