A well-designed API is a pleasure to use; a poorly designed one becomes a puzzle you have to re-solve with every request. In this article I gather the REST API best practices that genuinely help in day-to-day development — from resource naming and HTTP methods to status codes and versioning. The goal is a consistent, predictable interface that other developers (and you, six months from now) can guess without opening the docs.
Naming resources: nouns, not verbs
The core idea of REST is to model your system around resources. Endpoint paths should contain nouns, not verbs, and use plurals for collections. The action is expressed by the HTTP method; putting the action in the path is redundant.
- Good:
GET /users,GET /users/42,POST /users - Bad:
GET /getUsers,POST /createUser,GET /user/list
For nested resources, show the relationship in the path: a user's orders read naturally as GET /users/42/orders. But don't nest more than about two levels deep; paths like /users/42/orders/7/items/3/reviews become hard to maintain. Instead, offer direct access to the sub-resource: GET /order-items/3. Use lowercase and hyphens in URLs (/order-items), and avoid underscores or camelCase.
Use HTTP methods according to their meaning
Each method has an explicit contract, and honouring it lets intermediaries (caches, proxies, browsers) behave correctly.
- GET — reads a resource, no side effects (safe and idempotent).
- POST — creates a new resource; not idempotent (calling it twice creates two records).
- PUT — replaces the entire resource; idempotent (repeating the same request doesn't change the result).
- PATCH — updates part of a resource.
- DELETE — removes the resource; idempotent.
A practical rule: GET must never modify data. Putting a "delete" action on an endpoint like GET /users/42/delete is common but wrong, because a browser prefetch or a crawler could delete the record unknowingly. Prefer PUT for a full update and PATCH for a partial one.
Return the right status codes
The status code is the first signal of what happened, before the response body is even read. Always returning 200 OK and writing {"success": false} in the body blinds the client. The codes you'll use most:
- 200 OK — successful GET/PUT/PATCH.
- 201 Created — a resource was created via POST; return its address in the
Locationheader. - 204 No Content — success with no body (usually DELETE).
- 400 Bad Request — the request is malformed or failed validation.
- 401 Unauthorized — not authenticated; 403 Forbidden — authenticated but not permitted.
- 404 Not Found — the resource doesn't exist.
- 409 Conflict — a conflict (e.g. a duplicate email).
- 422 Unprocessable Entity — syntax is valid but semantic validation failed (many frameworks use this for validation errors).
- 429 Too Many Requests — rate limit exceeded.
- 500 Internal Server Error — an unexpected server-side error.
Use the difference between 401 and 403 correctly: the first means "I don't know who you are," the second means "I know who you are, but you're not allowed to do this."
Consistent, machine-readable errors
Returning every error in the same shape lets the client write a single error-handling routine. Provide a human-readable message, a stable machine code, and field-level validation details:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"error": {
"code": "validation_failed",
"message": "The submitted data is invalid.",
"fields": {
"email": ["Enter a valid email address."],
"age": ["Must be 18 or older."]
}
}
}
Keep the code field stable and language-independent (validation_failed), and use message for text you can show to a user. In production, never leak the internals of 500 errors (stack traces, SQL); instead return a request_id so the user can be matched with support.
Versioning: manage change without breaking it
From the moment your API is live, any change that breaks backward compatibility breaks existing clients. That's why a versioning strategy should be decided up front. The most common and most visible approach is to put the version in the path:
GET /v1/users
GET /v2/users
Alternatively you can carry the version in a header (Accept: application/vnd.api+json; version=2); this is considered more "pure" but is harder to discover and test. For most teams, URL-based versioning is practical and clear. The key principle: adding a field is usually non-breaking, but removing, renaming or changing the type of a field is breaking and requires a new version. Publish a deprecation timeline for old versions.
List endpoints: pagination, filtering, sorting
Returning a collection as-is works for small data sets, but as the table grows it overwhelms both server and client. Add pagination to list endpoints from the start. There are two common approaches:
- Offset-based:
GET /users?page=3&per_page=20— simple, allows jumping to a page number; but it drifts when data changes frequently. - Cursor-based:
GET /users?limit=20&cursor=eyJpZCI6MTQ0fQ— more stable and performant on large, frequently changing data sets.
Express filtering and sorting with query parameters: GET /users?status=active&sort=-created_at. A leading - in the sort is a common convention for descending order. Returning the total count and next-page info in a meta block makes it easier for the client to navigate.
A few more principles
- HTTPS everywhere: an API carrying tokens and personal data must run only over TLS.
- JSON consistency: stick to one field-naming style (
snake_caseorcamelCase) and don't change it across the API. - ISO 8601 and UTC for dates: the
2026-06-27T14:30:00Zformat removes ambiguity. - Documentation: publish a machine-readable schema with OpenAPI (Swagger), which gives you both docs and client-code generation.
- Rate limiting and authentication: add rate limiting to prevent abuse and report its state with the appropriate headers.
Frequently Asked Questions
Should I use PUT or PATCH?
Use PUT when you send the entire resource and replace it; use PATCH when you send only the few fields that change. PUT is idempotent: sending the same full body repeatedly doesn't change the result. PATCH is for partial updates, and its body carries only the fields to be changed.
Should I keep the version in the URL or a header?
Both are valid. URL-based versioning (/v1/...) is most teams' choice because it's easy to discover and test in a browser. Header-based versioning is closer to REST purity but is more cumbersome in terms of tooling support and visibility. As long as you're consistent, either is correct.
Can't I just return 200 for everything and signal errors in the body?
No. HTTP status codes exist precisely for this; cache layers, proxies and client libraries act on them. Using 4xx/5xx for errors lets the client make the right decision without parsing the response body, and it improves observability.
Want to design your API from scratch or bring existing endpoints up to standard? From resource modelling to versioning and documentation, I can help you build a clean, consistent REST interface. Get in touch and let's harden your project's API together.