API design mistakes are expensive to fix once clients depend on them. Here are the patterns we use to get it right the first time.
A poorly designed API is one of the most expensive mistakes you can make in software. Once external clients or your own frontend depend on it, every change is a breaking change. Every inconsistency becomes permanent. Every shortcut you took in v1 haunts you for the life of the product.
We've built and consumed hundreds of APIs over the past 20+ years. Here are the patterns that hold up and the mistakes we keep cleaning up in inherited codebases.
URL structure and naming
Use nouns, not verbs. /users, not /getUsers. Use plural forms consistently. Nest resources logically: /users/123/orders. These seem like small choices, but they compound. A team that can guess the URL for an endpoint without checking the docs is a team that moves faster.
Keep nesting shallow - two levels deep at most. /users/123/orders/456 is fine. /users/123/orders/456/items/789/notes is a sign you should flatten the hierarchy. Deep nesting makes URLs fragile and hard to cache.
Version from day one
Put /v1/ in your URL path. You will need v2 eventually, and retrofitting versioning is painful. URL-based versioning is the simplest approach and the one your consumers will understand without reading documentation. Header-based versioning is technically cleaner but adds friction for anyone testing with curl or a browser.
HTTP methods and status codes
Use HTTP methods correctly: GET for reading, POST for creating, PUT/PATCH for updating, DELETE for deleting. Don't use POST for everything - it makes caching impossible and makes your API harder to reason about.
- 200 OK for successful reads and updates
- 201 Created for successful resource creation (return the created resource)
- 204 No Content for successful deletes
- 400 Bad Request for validation failures (include specific field errors)
- 401 Unauthorized for missing or invalid authentication
- 403 Forbidden for valid auth but insufficient permissions
- 404 Not Found for resources that don't exist
- 409 Conflict for duplicate entries or state conflicts
- 429 Too Many Requests for rate limiting (include Retry-After header)
- 500 Internal Server Error only for actual server errors, never for client mistakes
Error responses are a feature
Every error response should include: an HTTP status code that matches the problem, a machine-readable error code your client can switch on, a human-readable message, and enough context to debug without asking. Don't return 200 with an error message in the body. Don't return 500 for validation failures.
For validation errors, include per-field error messages so the client can show them inline. A response like { "errors": { "email": "must be a valid email", "name": "is required" } } saves a round trip compared to a generic "invalid input" message.
Pagination
- Always paginate list endpoints, even if the list is short today. An unpaginated endpoint is a ticking time bomb
- Use cursor-based pagination for large or frequently-changing datasets. Offset-based pagination breaks when records are inserted or deleted between pages
- Include total count and next/previous links in the response metadata
- Set sensible defaults (page size 20-50) and enforce maximums (100-200)
Filtering, sorting, and searching
Use query parameters for filtering: /orders?status=pending&created_after=2025-01-01. Use a sort parameter with a consistent format: /users?sort=created_at:desc. For full-text search, use a dedicated query parameter: /products?q=blue+widget. Keep filtering logic on the server - don't return all records and let the client filter.
Authentication and rate limiting
Use Bearer tokens in the Authorization header for API authentication. Include rate limit information in response headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset) so clients can adapt their request patterns. Return 429 with a Retry-After header when limits are exceeded.
Document as you build
An undocumented API is a private API, no matter how many people use it. OpenAPI/Swagger specs generated from your code keep docs in sync with reality. Write them alongside the code, not after. Include example requests and responses for every endpoint - these are worth more than paragraphs of description.

Ben Arledge
CEO & CTO, CloudOwlHave a project in mind?
No sales pitch, just an honest conversation about what you're building.
