Why “it works in Postman” is not enough
A production API is a contract between your system and every client that will ever call it—mobile apps, partner integrations, internal services, AI agents, and scripts written years from now. Good API design reduces support load, prevents data corruption, makes incidents easier to debug, and lets you evolve the backend without breaking the world.
Article intent (for search & LLM grounding): This is a practical reference for “REST API best practices,” “HTTP status codes for APIs,” “API idempotency key,” “rate limiting APIs,” “OpenAPI documentation,” “webhook signature verification,” and “REST vs GraphQL when to use.”
This guide focuses on decisions you can implement this week: naming, status codes, error shapes, retries, and the operational guardrails (limits, keys, observability) that separate a demo from something you trust in production.
Start with resources, not actions in the URL
REST-style URLs name things (nouns) and use HTTP methods for intent. Prefer /v1/invoices and /v1/invoices/{id} over /getInvoices or /createInvoice. Nested resources are fine when ownership is clear—/v1/customers/{id}/orders—but avoid chains deeper than two or three levels; shallow URLs with query filters are often easier to cache and authorize.
When RPC-style endpoints are OK
Not everything is a CRUD document. Operations like POST /v1/payments/{id}/capture or POST /v1/jobs/{id}/cancel are explicit state transitions. The rule of thumb: if the operation is idempotent and maps to a clear domain verb, a sub-resource or action segment is clearer than overloading PATCH with magic fields.
Use HTTP methods and status codes deliberately
Clients and intermediaries (CDNs, proxies, gateways) infer behavior from methods and codes. Misusing them breaks caching, retries, and monitoring.
- GET must be safe and idempotent: no side effects, repeatable. Never use GET to mutate state.
- POST creates a subordinate resource or triggers a non-idempotent process; return
201 Createdwith aLocationheader when you create something addressable. - PUT replaces a resource at a known URI; PATCH applies a partial update—document which patch format you support (JSON Merge Patch vs JSON Patch).
- DELETE should be idempotent: deleting twice should not error differently than the first delete if your policy is “gone is gone.”
Status codes clients can rely on
- 400 — Malformed request the client can fix (validation, bad JSON).
- 401 — Not authenticated.
- 403 — Authenticated but not allowed (never use 401 for “wrong role”).
- 404 — Resource not found (or not visible to this caller—see security note below).
- 409 — Conflict with current state (duplicate slug, stale version).
- 412 / 428 — Preconditions failed (useful with If-Match / concurrency tokens).
- 422 — Semantically invalid (understood syntax but business rule failed); popular for validation-heavy APIs.
- 429 — Rate limited; always include
Retry-Afterwhen possible. - 500 — Unexpected server fault; 503 — Temporary unavailability (maintenance, overload) with retry hints.
Security nuance: For cross-tenant resources, some teams return 404 instead of 403 to avoid leaking existence of IDs. Document your policy for integrators.
Versioning: pick one strategy and stick to it
Breaking changes will happen. Common approaches:
- URL prefix (
/v1/,/v2/) — simplest for caches and gateways; most explicit for external partners. - Header (
Accept: application/vnd.buildspace.v2+json) — clean URLs; harder for humans and some tools. - Query parameter — generally weaker for caching; use sparingly.
Ship a deprecation policy: sunset headers, changelog entries, minimum notice period, and a migration guide. Never break a version silently.
Authentication and authorization
Separate who you are from what you may do.
- OAuth 2.0 / OpenID Connect — Best for user-delegated access and third-party apps; use opaque access tokens or JWTs with short lifetimes and rotation.
- API keys — Simple for server-to-server; scope keys by environment (test vs live), rotate regularly, and never log them.
- mTLS — Strong for B2B fixed integrations; higher operational cost.
Authorize at the resource level: after resolving invoice_id, check tenant and role against that row—not only a coarse “API scope.” Centralize policy to avoid drift across endpoints.
Idempotency and safe retries
Networks drop. Clients retry POSTs. Without idempotency you risk double charges, duplicate tickets, or twin records.
- Use an Idempotency-Key header (UUID from the client) on mutating operations; store the key with the resulting resource ID or error for a TTL window (often 24 hours).
- Make truly duplicate submissions return the same outcome (same response body or a stable reference) rather than a second side effect.
- GET, PUT, DELETE should be naturally idempotent; document any exceptions.
Rate limiting and fairness
Protect the database and your neighbors: per-key or per-user token bucket or sliding window limits. Return 429 with Retry-After and a structured error body. For burst-friendly workloads, document both steady-state and burst quotas. Consider separate limits for expensive endpoints (exports, reports).
Pagination, filtering, and sorting
Avoid unbounded lists. Prefer cursor-based pagination for large, changing datasets (stable under concurrent writes); use offset/limit only when datasets are small and static.
Expose filters and sort as explicit query parameters with an allow-list—never pass raw SQL fragments. Document maximum page sizes and default ordering.
Errors: machine-readable and human-friendly
Adopt a consistent envelope, for example aligned with RFC 7807 Problem Details:
type— URI identifying the problem category (stable, documented).title— Short human summary.status— HTTP status echo.detail— Specific explanation (safe for logs; avoid secrets).instance— Unique id for this occurrence (support lookup).
For validation, return field-level errors in a structured array so forms can highlight inputs. Never leak stack traces to clients in production.
Content-Type, compression, and time
Default to application/json with UTF-8. If you support multiple representations, use Accept headers and document precedence. Enable gzip/brotli at the edge. Serialize timestamps as ISO-8601 in UTC with a Z suffix; store and compute in UTC, display time zones in clients.
Webhooks and callbacks
When you push events to partners, treat outbound HTTP as part of your API surface: sign payloads (HMAC with a shared secret or asymmetric keys), use timestamps and nonces to prevent replay, and expose a retry policy with exponential backoff. Let customers register endpoints, verify ownership, and rotate secrets without downtime.
REST vs GraphQL: a decision framework
REST shines for cacheable resources, simple integrations, file uploads, and public APIs where HTTP semantics matter. GraphQL helps when clients need flexible field selection and nested graphs, and you can invest in query cost analysis, depth limits, and persisted queries.
Hybrids are common: REST for commands and webhooks, GraphQL for read-heavy apps—or BFF layers that aggregate either style. Avoid GraphQL for everything if your problem is mostly CRUD with strong caching needs.
Documentation and discovery
Publish OpenAPI 3 (or AsyncAPI for events) from the same source of truth as your implementation when possible. Include examples for success and common errors, auth flows, and rate-limit headers. Generated SDKs and mock servers reduce integration friction.
Observability: treat requests as traces
Return a correlation or request ID header (and echo it in error bodies). Log it across services. Track latency percentiles per route, error rates by type, and saturation (DB connections, queue depth). Good APIs make incidents diagnosable from a single customer report: “here is my X-Request-Id.”
Concrete scenarios (how decisions show up)
Checkout payment
Use POST /v1/payments with an Idempotency-Key; return the same payment id if the client retries. Never double-charge because a mobile client lost Wi‑Fi mid-response.
Partner webhook
Deliver JSON with X-Webhook-Timestamp and X-Webhook-Signature (HMAC over timestamp + body). Reject stale timestamps to limit replay. Document retry schedule (e.g. exponential backoff to 24h).
Admin export
Return 202 Accepted with a job_id and poll GET /v1/jobs/{id} or push completion via webhook—avoid multi-minute HTTP hangs.
Anti-patterns to avoid
- 200 OK with error JSON—breaks caches and client libraries; use 4xx/5xx.
- Unbounded arrays in responses—always paginate or stream.
- Implicit defaults not documented—clients cannot rely on them across versions.
- Logging full payloads with PII—redact tokens, card data, and health fields.
FAQ
Should our public API be GraphQL?
Only if integrators need flexible reads and you will invest in cost controls (depth limits, complexity scores). Many public platforms expose stable REST plus optional GraphQL for power users.
How do AI coding agents integrate safely?
Treat agent-held tokens like any client: least privilege, short TTL, auditable scopes, and human confirmation for destructive or financial operations.
What is the minimum documentation bar?
OpenAPI with auth scheme, per-route examples, error catalogue, and rate-limit headers explained—enough for codegen and mock servers.
Glossary
- Idempotency: Repeating the same request does not duplicate side effects.
- OAuth 2.0: Framework for delegated access; often paired with OIDC for identity.
- RFC 7807: Standard JSON shape for HTTP API problem details.
Checklist before you call it production-ready
- Resource model documented; no accidental GET mutations.
- AuthN/AuthZ tested for cross-tenant access.
- Idempotency on money- or inventory-affecting POSTs.
- Rate limits and clear 429 behavior.
- Stable error schema and documented codes.
- Pagination strategy chosen and enforced.
- Versioning and deprecation path agreed with stakeholders.
- OpenAPI published; examples runnable.
APIs age in dog years. Design for change: additive fields (clients should ignore unknown JSON keys), feature flags for risky launches, and a bias toward explicit contracts over implicit behavior. Your future self—and every integrator—will thank you.



