Articles › Software Architecture
REST API Design Best Practices: URLs, Methods, Status Codes and Versioning
Design APIs developers love: resource naming, HTTP methods, status codes, error format, pagination, filtering, versioning, idempotency and security — with concrete examples.
An API is a user interface for developers. A well-designed API is predictable: once you've used two endpoints, you can guess how the third works. A poorly designed one forces everyone to read documentation for every call — and breaks clients whenever it changes. These are the conventions that make REST APIs a pleasure to consume.
Think in resources, not actions
REST models your system as resources (nouns) manipulated with a small set of HTTP methods (verbs).
| ❌ RPC-style | ✅ Resource-oriented |
|---|---|
POST /getProducts |
GET /products |
POST /createOrder |
POST /orders |
GET /deleteUser?id=5 |
DELETE /users/5 |
POST /updateProductPrice |
PATCH /products/42 |
Naming rules
- Use plural nouns:
/products,/orders. - Use kebab-case for multi-word paths:
/order-items. - Nest only for real ownership, and not deeper than one level:
/customers/7/orders. - Keep URLs lowercase; never put verbs in them except for genuine operations that aren't CRUD (
POST /orders/42/cancelis acceptable and clear).
Use HTTP methods correctly
| Method | Purpose | Safe | Idempotent |
|---|---|---|---|
GET |
read a resource or collection | ✅ | ✅ |
POST |
create a resource / trigger an operation | ❌ | ❌ |
PUT |
replace a resource entirely | ❌ | ✅ |
PATCH |
partially update a resource | ❌ | usually ❌ |
DELETE |
delete a resource | ❌ | ✅ |
- Safe means it doesn't change state —
GETmust never modify data (crawlers and prefetchers call GETs freely). - Idempotent means repeating the request has the same effect as doing it once — important when networks retry.
Return meaningful status codes
| Code | When |
|---|---|
200 OK |
successful GET / PUT / PATCH with a body |
201 Created |
resource created — include a Location header |
204 No Content |
success without a body (DELETE, some updates) |
400 Bad Request |
malformed request or validation failure |
401 Unauthorized |
not authenticated (missing/invalid token) |
403 Forbidden |
authenticated but not allowed |
404 Not Found |
resource doesn't exist |
409 Conflict |
state conflict (duplicate email, version mismatch) |
422 Unprocessable Entity |
well-formed but semantically invalid (some teams use 400 instead) |
429 Too Many Requests |
rate limit exceeded |
500 Internal Server Error |
unexpected server failure |
Never return 200 OK with { "success": false } — it breaks HTTP tooling, caching and monitoring.
A consistent error format
Use the standard Problem Details format (RFC 9457), which ASP.NET Core supports out of the box:
{
"type": "https://example.com/problems/validation",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"email": ["Email is required."],
"price": ["Price must be greater than zero."]
},
"traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00"
}
One error shape across the whole API means clients write error handling once. Include a traceId so support can find the request in your logs — but never leak stack traces in production.
Pagination, filtering and sorting
Collections must be paginated:
GET /products?page=2&pageSize=20
GET /products?category=laptops&minPrice=500&sort=-price,name
{
"items": [ ... ],
"page": 2,
"pageSize": 20,
"totalCount": 438
}
- Offset pagination (
page/pageSize) is simple and supports jumping to a page. - Cursor pagination (
?after=eyJpZCI6MTAwfQ) is stable and fast for large or frequently changing data, like feeds. - Enforce a maximum
pageSizeon the server. - A common sorting convention:
sort=-createdAtfor descending.
Versioning
APIs evolve; clients don't update instantly. Plan versioning from day one:
GET /api/v1/orders/42 # URL versioning — simple and visible
GET /api/orders/42
api-version: 2.0 # header versioning — cleaner URLs
Rules that avoid most version bumps:
- Additive changes are safe: new endpoints, new optional fields.
- Breaking changes need a new version: removing or renaming fields, changing types or meaning.
- Announce deprecations with a
Deprecation/Sunsetheader and a timeline.
Idempotency for POST
Mobile networks retry. If a "create payment" request times out and is retried, you might charge twice. Accept an idempotency key:
POST /payments
Idempotency-Key: 6f1c9c0e-8f4b-4c6e-9b4a-2f9b1d1e7a10
The server stores the key with the result; a repeated request with the same key returns the original result instead of executing again.
Shape responses for clients
- Return DTOs, not database entities.
- Use consistent casing (
camelCasein JSON is the norm). - Use ISO 8601 for dates in UTC:
"2026-02-07T09:30:00Z". - Represent money with decimal strings or integer minor units — never binary floats.
- Use
nulldeliberately, and don't change the meaning of a field between versions.
Caching and concurrency
GET /products/42
ETag: "a3f9c2"
Cache-Control: public, max-age=60
- Clients can send
If-None-Match: "a3f9c2"and get304 Not Modified— saving bandwidth. - For updates,
If-Matchwith the ETag prevents lost updates: if someone else changed the resource, return412 Precondition Failed.
Security basics
- HTTPS everywhere.
- Authenticate with tokens (JWT/OAuth2) and authorize every endpoint — check ownership, not just login (prevent IDOR:
/orders/43must not be readable by user 42's token if it's not their order). - Validate all input; never trust client-provided IDs or prices.
- Rate-limit public endpoints and return
429withRetry-After. - Don't expose internal details (SQL errors, stack traces, server versions).
Document it
Generate an OpenAPI document (Swagger) and keep examples in it. Good documentation includes authentication instructions, error formats, pagination rules and realistic sample requests. Tools can then generate typed clients for TypeScript, C# and mobile apps.
Summary
Good REST APIs are resource-oriented, use HTTP methods and status codes as intended, return one consistent error format, paginate collections, version deliberately and protect every endpoint. These conventions aren't bureaucracy — they're what make an API predictable, evolvable and pleasant to integrate with.
FAQ
Should I use PUT or PATCH for updates?
Use PUT when the client sends the full representation, PATCH for partial updates. Many APIs only implement PATCH-style partial updates — just be consistent.
Is REST still relevant with GraphQL and gRPC?
Yes. REST is the simplest, most widely supported choice for public and web APIs. GraphQL suits complex client-driven data needs; gRPC suits internal service-to-service communication.
Should my API return 404 or 403 for resources the user can't access?
Returning 404 avoids revealing that the resource exists. Many APIs prefer that for private resources.