This is a list of guidelines I’ve been following to write APIs that behave in predictable patterns and are easy to use for fellow developers.
Since vibe coding is a real thing now, there’s a short prompt at the end to steer LLM coding tools to follow these principles.
What’s an API contract?
Contract = shape + semantics + guarantees
- Shape: schema (OpenAPI/JSON Schema, Protobuf for gRPC).
- Semantics: field meaning, errors, pagination, filtering, rate-limits, idempotency.
- Guarantees: versioning policy, auth model, latency/SLOs, backward compatibility.
Golden rules
- Versioning: path-based (
/v1/...
). Deprecate with dates; only additive changes in place.
- Errors: consistent HTTP 4xx/5xx + machine-parsable bodies (RFC 9457 problem+json).
- Observability:
X-Request-Id
, trace context, structured logs.
- LROs: return
202 Accepted
+ operation id + polling endpoint or signed webhook (with retries, idempotent).
Type |
Example |
Pros |
Cons |
Use when |
Offset/Page |
?page=12&limit=50 or ?offset=550&limit=50 |
Simple; countable |
Slow on big tables; dupes/misses under churn; costly COUNT |
Small sets, admin UIs needing totals |
Cursor/Keyset (seek) |
?after=<opaque_cursor>&limit=50 |
Fast; stable under inserts/deletes; index-friendly |
No random access; needs stable sort keys |
Feeds, ledgers, logs, infinite scroll |
Time-window |
?from=2025-08-01T00:00Z&to=... |
Natural for logs/events; partition pruning |
Clock skew; ties on same timestamp |
Audit/event APIs, ingestion windows |
Snapshot token |
?page_token=<snapshot_id:cursor> |
Fully consistent page-through |
Snapshot/token management |
Reports/analytics under heavy churn |
GraphQL connections |
edges{cursor,node{...}} pageInfo{...} |
Standardized cursor model |
Requires GraphQL infra |
GraphQL servers/clients |
Notes: keep ordering stable (e.g., (created_at,id)
), cursors are opaque (base64 of last seen keys), cap limit
.
1
|
{ "data":[...], "page": { "next_cursor":"…", "has_more": true } }
|
Filtering & sorting
- Whitelisted filters only:
filter[status]=paid&filter[amount][gt]=100
.
- Allowed ops:
eq, ne, gt, gte, lt, lte, in
.
- Sort:
sort=-created_at,amount
(include a tiebreaker like id
).
- Keep text search (
q=
) separate from filters; index what you expose.
Idempotency
- Natural:
PUT /resource/{id}
, DELETE /resource/{id}
are retry-safe.
- POST + Idempotency-Key: client sends
Idempotency-Key: <uuid>
; server stores final outcome and replays the same response on retry (TTL ≥ retry window).
- Optimistic concurrency:
If-Match: <etag|version>
on updates to avoid lost writes.
- Events/Webhooks: dedupe by delivery id (inbox/outbox).
1
2
3
4
5
|
POST /v1/orders
Idempotency-Key: 8b8a6b38-...
201 Created
Location: /v1/orders/ord_abc123
|
Rate limits & quotas
- Prefer token bucket per API key/tenant (burst-friendly). Optionally per-IP for public endpoints. Weight by cost (e.g., upload=10 tokens).
- Communicate limits:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 123
X-RateLimit-Reset: 1723360800
- Return
429 Too Many Requests
with Retry-After
.
Backpressure (stay fast under load)
- Admission control: cap in-flight requests; reject with
503
or 429
.
- Bounded queues only; when full, shed work (don’t block forever).
- Deadlines/timeouts end-to-end; cancel downstream when client deadline nears.
- Bulkheads: isolate pools/queues by function/tenant.
- Circuit breakers for flaky dependencies.
- Async handoff for heavy jobs:
202
+ operation status or webhook.
Authn/Z & tenancy
- Authn: JWT (short-lived) or opaque tokens. Validate
iss, aud, exp, nbf
; rotate keys (kid
, JWKS).
- Authz: RBAC or ABAC; enforce server-side.
- Always bind queries by tenant_id (don’t trust client-provided filters).
1
|
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6…
|
Errors & validation
- Use problem+json; prefer
422
for validation failures.
1
2
3
4
5
6
7
|
{
"type":"https://docs.example.com/errors/validation",
"title":"Validation failed",
"status":422,
"errors":[{"field":"amount","msg":"must be >= 0"}],
"instance":"req_7f1a..."
}
|
Caching & conditional requests
- Support
ETag/If-None-Match
and/or Last-Modified/If-Modified-Since
.
- Separate public vs user-specific cache; set
Cache-Control
clearly.
Long-running operations & webhooks
- Return
202 Accepted
+ Location: /v1/operations/{id}
; allow polling.
- Webhooks: sign (HMAC), retry with backoff, receivers must be idempotent.
OpenAPI & JSON Schema
- OpenAPI 3.1 to describe endpoints, params, auth, errors.
- Use JSON Schema 2020-12 for payloads;
$ref
shared schemas across services and docs.
- Generate SDKs, mocks, and validation from the spec.
Choose-by-use-case (cheat sheet)
- Create payment/booking → POST + Idempotency-Key (+ outbox).
- Upsert config → PUT /resource/{id}.
- Infinite feed → Cursor pagination
(created_at,id)
.
- Reporting export → 202 + snapshot token pagination.
- Expensive writes → Async + bounded queue, backpressure + quotas.
Vibe-coding prompt (drop into Claude Code/Cursor)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
You are an staff software engineer at a big tech company.
Generate production-grade HTTP APIs that follow these rules:
- Use resource-oriented design, versioned paths (/v1).
- Methods: GET/HEAD safe+cacheable; PUT/DELETE idempotent; POST uses Idempotency-Key.
- Lists: cursor pagination (stable order by created_at,id), filters (whitelisted ops eq,ne,gt,gte,lt,lte,in), sort with tiebreakers.
- Errors: RFC 9457 problem+json; use 422 for validation.
- Auth: Bearer JWT (validate iss,aud,exp,nbf), enforce RBAC/ABAC server-side; bind by tenant_id.
- Rate limits: token bucket per API key with X-RateLimit-* headers; return 429 + Retry-After.
- Backpressure: bounded queues, concurrency caps, timeouts, circuit breakers; use 202 + operation status for heavy jobs.
- Caching: ETag/If-None-Match on GET.
- Emit Request-Id and tracecontext; structured logs.
Deliver:
1) OpenAPI 3.1 spec with JSON Schemas and examples.
2) Minimal server stubs (Go/TypeScript) with middleware for request-id, JWT verify, idempotency, cursor helpers, limiter.
3) Brief README with usage and curl examples.
|