Most API design advice is either too academic ("here are 47 REST constraints from Roy Fielding's thesis") or too vague ("make it intuitive"). Here are the practices that actually affect developer experience and system reliability in production.
Consistent Resource Naming
Use plural nouns for collections, singular for sub resources. /users returns a list, /users/123 returns one user. Nest related resources: /users/123/orders returns orders for user 123.
Use kebab-case for URLs. /user-profiles not /userProfiles or /user_profiles. URLs are case insensitive in practice, and kebab-case is the web convention.
Be predictable. If /users returns a list and /users/123 returns a single user, then /products should return a list and /products/456 should return a single product. Every deviation from the pattern forces developers to check the docs.
Error Responses That Help
Bad error response: 500 Internal Server Error with no body. Developers see this, have no idea what went wrong, and file a support ticket.
Good error response:
Status: 422 Unprocessable Entity with a JSON body containing: error code ("validation_error"), human readable message ("Email address is already registered"), field level details (field: "email", message: "Already in use"), and a documentation URL.
Use appropriate HTTP status codes. 400 for client input errors, 401 for unauthenticated, 403 for unauthorized, 404 for not found, 409 for conflicts, 422 for validation failures, 429 for rate limiting, 500 for server errors. Most APIs only need these 8 codes.
Make errors actionable. "Invalid request" tells the developer nothing. "The 'start_date' field must be a date in ISO 8601 format (e.g., 2026-01-15)" tells them exactly how to fix it.
Pagination
Never return unbounded lists. Even if your table has 50 rows today, it will have 50,000 eventually.
Cursor based pagination for large or frequently changing datasets. Return a cursor (opaque token) with each response that the client passes to get the next page. This handles inserts between page requests correctly.
Offset pagination for simple use cases where consistent results are not critical. Easier to implement (/users?page=2&per_page=25) and supports jumping to arbitrary pages.
Always return pagination metadata: total count, next page cursor/URL, has_more boolean. Front end developers need this to build pagination UI without extra API calls.
Authentication
API keys for server to server communication. Simple, no expiration dance, easy to rotate. Include in headers (Authorization: Bearer sk_live_xxx), never in URL parameters (they end up in logs).
OAuth 2.0 / JWT for user authenticated requests. Access tokens expire in 15-60 minutes, refresh tokens handle renewal. JWTs let you embed user claims (role, tenant ID) in the token itself, reducing database lookups per request. This is the pattern we use on Traderly and most SaaS products.
Never roll your own auth. Use Supabase Auth, Auth0, or a proven library. Every custom authentication system we have audited had at least one critical vulnerability.
Rate Limiting
Rate limiting protects your API from abuse and ensures fair usage across clients.
Return rate limit headers. X-RateLimit-Limit (max requests per window), X-RateLimit-Remaining (requests left), X-RateLimit-Reset (when the window resets). This lets clients implement backoff without guessing.
Different limits per endpoint. Authentication endpoints: 10 requests/minute (prevent brute force). Read endpoints: 1,000 requests/minute. Write endpoints: 100 requests/minute. Cost intensive operations (AI, file processing): 10 requests/minute.
Return 429 with Retry-After header when limits are exceeded. Include the retry time in the response body too, not every client reads headers.
Versioning
You will need to make breaking changes eventually. Plan for it.
URL versioning (/v1/users, /v2/users) is the simplest and most explicit approach. Clients know exactly which version they are using. This is our default recommendation.
Header versioning (Accept: application/vnd.api+json;version=2) is cleaner but harder for developers to test (you cannot test it in a browser URL bar). Use this for internal APIs where you control all clients.
Additive changes are not breaking changes. Adding a new field to a response, adding a new endpoint, or adding an optional parameter to a request are all backward compatible. Only remove or rename fields in a new version.
Idempotency
For any operation that creates or modifies resources, support idempotent requests. The client sends an Idempotency-Key header, and if the same key is sent again, the server returns the original response without re executing the operation.
This is critical for payment processing, order creation, and any operation where a network timeout might cause a retry. Without idempotency, a retry after timeout creates a duplicate order or double charges a customer.
Stripe pioneered this pattern and it works well: store the idempotency key and response for 24 hours, return the cached response on duplicate keys.
Documentation
OpenAPI (Swagger) spec as the source of truth. Generate it from your code (if using TypeScript, tools like tsoa or Zod-to-OpenAPI) to keep docs and implementation in sync. Stale documentation is worse than no documentation.
Working examples for every endpoint. Not just the request format, full curl commands with authentication headers, realistic request bodies, and annotated response bodies. Developers copy paste examples, not schema definitions.
Our system architecture practice designs APIs that scale from MVP to millions of requests. We apply these patterns consistently across projects because they work, check our real time architecture guide for how these principles extend to WebSocket and event driven APIs.
Designing an API and want to get it right? Talk to us before you ship v1.