A REST API is a public contract. Every design decision you make is a commitment to everyone who builds on it. The patterns that cause the most regret in production are the same ones every team learns the hard way: inconsistent naming, undocumented error codes, breaking versioning strategies, and pagination designs that fail at scale.
Naming and resource modeling: use plural nouns for resource collections (`/users`, not `/user`). Represent relationships with nested paths up to one level deep (`/users/{id}/posts`), then switch to query parameters for deeper nesting. Use kebab-case for URL paths (`/api/rate-limits`), camelCase for JSON property names (`userId`, `createdAt`). Return resource identifiers as strings, not integers — integer IDs leak database implementation details and cause precision loss in JavaScript's number type above 2^53.
Error format: use RFC 7807 (Problem Details for HTTP APIs) as your error schema. Every error response includes `type` (a URI identifying the error class), `title` (human-readable summary), `status` (HTTP status code), `detail` (instance-specific description), and optionally `instance` (URI for the specific error occurrence). Clients can pattern-match on `type` for error handling logic. This is the format Stripe, GitHub, and most modern APIs use in some form.
Versioning: URI versioning (`/v1/users`) is the most explicit and caching-friendly. Header versioning (`Accept: application/vnd.api+json; version=2`) is cleaner but harder to test. Pick URI versioning and commit to it. Never break a versioned endpoint — add a new version rather than changing existing behavior. Keep older versions alive for at least 12-18 months with documented sunset dates.
Pagination: cursor-based pagination is strictly better than offset pagination for any collection that can grow or change during traversal. Offset pagination returns wrong results if items are inserted or deleted mid-traversal. Use opaque cursor tokens (`?after=dXNlcjox`) that clients don't interpret. Return `nextCursor` in the response; absence of the field means no more results. For small, stable collections (under 1K items), offset pagination is acceptable.
Authentication: Bearer tokens in the Authorization header for all API authentication. Use JWTs for stateless authorization (no database lookup per request) or opaque tokens with a lookup store. API keys for machine-to-machine: prefix them with a human-readable identifier (`sk_live_`, `ghp_`) so security scanners can detect leaks. Never put credentials in query parameters — they appear in server logs.
Rate limiting response headers: return `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` on 429 responses. These are not standardized by RFC but are widely adopted. Without them, clients have no choice but to implement exponential backoff blindly.