A well-designed API is one that developers can figure out without reading the docs. That might sound like an exaggeration, but it is the standard worth aiming for. After building and consuming dozens of APIs over the past few years, I have collected a set of practices that consistently lead to better outcomes.
Use Nouns for Resources, Not Verbs
This is the most commonly violated rule in REST API design. The URL should represent a resource, and the HTTP method should represent the action.
// Bad
POST /createUser
GET /getUserById/123
POST /deleteUser/123
// Good
POST /users
GET /users/123
DELETE /users/123
The HTTP method already tells you the action. Putting a verb in the URL is redundant and makes the API inconsistent. You end up with a mix of naming patterns that force consumers to memorize each endpoint individually instead of following a predictable convention.
Be Consistent With Naming
Pick a convention and stick with it across your entire API. If you use plural nouns for collections (/users, /orders), do not suddenly switch to singular (/product/123). If you use kebab-case for multi-word resources (/order-items), do not use camelCase or snake_case elsewhere.
Consistency reduces cognitive load for your consumers. They should be able to guess the endpoint for a new resource based on the patterns they have already seen.
Version Your API From Day One
Every API will change. If you do not version from the start, you will eventually need to make a breaking change and have no clean way to do it.
GET /v1/users/123
GET /v2/users/123
I prefer URL-based versioning over header-based versioning. It is more visible, easier to test with a browser or curl, and simpler to route in most API gateways. The counterargument is that the URL should represent a resource, not a version - but in practice, URL versioning is what most teams find easiest to manage.
Use Proper HTTP Status Codes
Do not return 200 for everything. Status codes exist to communicate the result of the request without requiring the consumer to parse the response body.
- 200 - Success with response body
- 201 - Resource created successfully
- 204 - Success with no response body (common for DELETE)
- 400 - Client sent a bad request (validation error)
- 401 - Not authenticated
- 403 - Authenticated but not authorized
- 404 - Resource not found
- 409 - Conflict (duplicate resource)
- 422 - Unprocessable entity (valid syntax but semantic errors)
- 429 - Rate limit exceeded
- 500 - Server error
A good status code lets automated systems (monitoring tools, retry logic, client libraries) handle responses correctly without understanding your specific error format.
Design Error Responses Carefully
When something goes wrong, the error response should tell the consumer exactly what happened and what they can do about it.
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "email",
"message": "Must be a valid email address"
},
{
"field": "age",
"message": "Must be a positive integer"
}
]
}
}
Include a machine-readable error code, a human-readable message, and - for validation errors - details about which fields failed and why. This saves consumers from guessing what went wrong.
Pagination Is Not Optional
Any endpoint that returns a list must support pagination. An unbounded list endpoint will work fine with 10 records and bring down your database with 10 million.
GET /users?page=2&limit=20
{
"data": [...],
"pagination": {
"page": 2,
"limit": 20,
"total": 1547,
"pages": 78
}
}
For large datasets or real-time feeds, cursor-based pagination is more reliable than offset-based because it handles insertions and deletions between pages correctly.
Filter, Sort, and Search via Query Parameters
Keep your base URL clean and use query parameters for refinement.
GET /orders?status=shipped&sort=-created_at&q=wireless
The minus sign prefix for descending sort is a common convention. It keeps things readable without requiring a separate sort_direction parameter.
Return Created Resources
When a POST request creates a resource, return the full created resource in the response. This saves the consumer from making a second GET request to fetch what they just created. Include the server-generated fields like id, created_at, and any computed values.
Document As You Build
Use OpenAPI (formerly Swagger) to document your API alongside the code. Tools like swag for Go or drf-spectacular for Django can generate OpenAPI specs from your code annotations. The documentation stays in sync with the implementation because it is derived from it.
A well-designed API reduces support requests, speeds up integration, and makes your system easier to evolve. Invest the time upfront. It pays for itself many times over.