REST API design examples: routes, methods, status codes, and errors
Quick answer
💡A well-designed REST API uses nouns not verbs in resource paths, HTTP methods to express intent (GET read, POST create, PUT replace, PATCH update, DELETE remove), correct status codes (201 Created with Location header, 204 No Content for deletes), and a consistent error response format. Use /tools/http-request-builder to prototype and test resource routes before writing any server code.
Error symptoms
- ✕
Routes include verbs like /getUser or /createOrder instead of resources - ✕
POST endpoints return 200 OK instead of 201 Created with a Location header - ✕
DELETE endpoints return the deleted resource body instead of 204 No Content - ✕
Error responses have different shapes across endpoints, breaking client error handling - ✕
Pagination breaks when items are inserted concurrently between page requests - ✕
PUT and PATCH used interchangeably when only one is appropriate
Common causes
- •RPC mental model applied to HTTP, treating endpoints as function calls
- •No defined error response schema, so each endpoint author invents their own
- •Using offset-based pagination without understanding its instability under concurrent writes
- •Returning 200 with a success: false body instead of an appropriate 4xx status code
- •Versioning mixed inconsistently between URI, header, and query parameter strategies
- •PUT used for partial updates, leaving unset fields as null in the stored resource
When it happens
- •Designing a new API without a reference standard or style guide
- •Incrementally adding endpoints to an existing RPC-style API
- •Migrating from SOAP or GraphQL where the mental model differs from REST
- •Building a public API that external developers will consume
- •Implementing pagination for a resource that receives frequent writes
Examples and fixes
curl, JavaScript fetch, and Python requests showing correct REST resource design.
Resource naming and correct status codes
❌ Wrong
# curl — verb in path, wrong status code on create
curl -X POST https://api.example.com/createUser \
-H "Content-Type: application/json" \
-d '{"name":"Alice","email":"[email protected]"}'
# Server returns 200 OK {"success":true,"userId":42}
// JS — no status check, no Location header consumed
const res = await fetch("/api/getAllUsers");
const users = await res.json();
# Python — action-oriented endpoint, wrong delete response
requests.get("https://api.example.com/v1/deleteOrder?id=99")✅ Fixed
# curl — noun path, capture Location header from 201 response
curl -i -X POST https://api.example.com/v1/users \
-H "Content-Type: application/json" \
-d '{"name":"Alice","email":"[email protected]"}'
# Server returns: 201 Created
# Location: /v1/users/42
// JS — correct collection path, check status before parsing
const res = await fetch("/api/v1/users");
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const users = await res.json();
# Python — REST delete using method and path, expect 204
res = requests.delete("https://api.example.com/v1/orders/99",
headers={"Authorization": "Bearer YOUR_TOKEN_HERE"},
timeout=10)
assert res.status_code == 204The broken examples encode the action in the URL, which duplicates information already carried by the HTTP method. /createUser and /getAllUsers make it impossible to apply uniform routing logic, caching rules, or rate limits. A GET request to /getAllUsers cannot be distinguished from a resource fetch by infrastructure layers. The fixed versions use noun-based paths. POST /v1/users returns 201 Created with a Location header pointing to the new resource, so clients can fetch the created item without parsing a response body. DELETE returns 204 No Content with an empty body, which is the correct semantic. Test these patterns using /tools/http-request-builder to verify status codes and response headers match the design.
Structured error format and safe retry with an idempotency key.
Consistent error responses and idempotency keys
❌ Wrong
// Inconsistent error shapes across endpoints
// Orders endpoint
fetch("/v1/orders", { method: "POST" })
// Returns: { "success": false, "msg": "validation error" }
// Users endpoint
fetch("/v1/users", { method: "POST" })
// Returns: { "error": true, "errors": ["email required"] }
// Unsafe retry: POST without idempotency key creates duplicate orders
for (let i = 0; i < 3; i++) {
await fetch("/v1/orders", {
method: "POST",
body: JSON.stringify({ productId: 5, qty: 1 })
});
}✅ Fixed
// Consistent error format across all endpoints
// { "error": { "code": "VALIDATION_ERROR", "message": "...", "details": [] } }
// Orders endpoint — returns structured error
const res = await fetch("/v1/orders", { method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ productId: 5, qty: 1 })
});
if (!res.ok) {
const err = await res.json();
// err.error.code is always a string, err.error.details is always an array
throw new Error(err.error.message);
}
// Safe retry: Idempotency-Key prevents duplicate order creation
const idempotencyKey = crypto.randomUUID();
const order = await fetch("/v1/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey
},
body: JSON.stringify({ productId: 5, qty: 1 })
});
// Safe to retry: same key returns same response without creating a duplicateWhen error response shapes vary per endpoint, client error handling code must branch on which endpoint was called rather than on the error structure. This couples the client tightly to implementation details. The fixed version uses { error: { code, message, details[] } } consistently across every endpoint. Code is a machine-readable string, message is human-readable, and details is always an array for zero or more validation problems. The idempotency key pattern makes POST operations safe to retry. The server uses the key to detect duplicate requests and return the original response instead of processing again. This is essential for payment, order, and inventory endpoints where duplicate processing causes real business harm.
REST constraints that guide good design
REST, which stands for Representational State Transfer, is an architectural style defined by Roy Fielding in his 2000 dissertation. It describes six constraints that, when followed together, produce APIs that are scalable, cacheable, and easy for clients to consume without prior knowledge of the implementation.
The most impactful constraint for day-to-day API design is the uniform interface requirement. It has four sub-constraints: resources must be identified by URIs, clients manipulate resources through representations, messages must be self-descriptive, and hypermedia must be the engine of application state. In practice, teams implement the first two well but often skip the latter two entirely, which is why HATEOAS is rarely seen in production despite being part of the original REST definition.
Statelessness is the second most important constraint. Every request must contain all information needed to understand and process it. The server must not store session state between requests. This is why REST APIs use bearer tokens in the Authorization header rather than server-side sessions: the token carries identity information in the request itself without requiring the server to look up a session store.
Cacheability means that responses must explicitly state whether they can be cached. GET responses should include Cache-Control headers. POST, PUT, PATCH, and DELETE responses typically cannot be cached. When clients and infrastructure intermediaries can cache GET responses, backend load drops significantly without any application changes.
The layered system constraint allows load balancers, CDNs, API gateways, and proxies to sit between clients and servers without either knowing about the other. This works only when APIs are stateless and use standard HTTP semantics. Custom headers that bypass caching or routing logic violate this constraint.
These constraints produce specific design decisions. Resources should be named with nouns, not verbs, because the HTTP method already carries the action. /users is a collection, /users/42 is a specific user. The HTTP method determines what happens: GET reads without side effects, POST creates, PUT replaces a resource entirely at a known URI, PATCH modifies specific fields, and DELETE removes. Using GET for mutations or POST for every operation violates the uniform interface and breaks caching.
Spotting RPC patterns disguised as REST
The quickest way to identify non-REST patterns in an existing API is to list all endpoint paths and look for verbs. Paths like /getUser, /createOrder, /updateProfile, /deleteItem, /fetchAll, /search, and /process are RPC patterns. Each encodes an action that HTTP already expresses through methods. These paths make it impossible to apply uniform caching, rate limiting, or authorization rules because each is semantically distinct from every other.
The second check is status codes. An API that returns 200 OK for both successful creates and validation errors is using HTTP as a transport pipe rather than a protocol. Clients must parse every response body to determine success or failure, which defeats the purpose of standardized status codes. Use curl -i to see status codes directly. A create endpoint should return 201. A delete endpoint should return 204. A request with invalid input should return 422 Unprocessable Entity rather than 200 with success: false.
The third check is error response consistency. Request errors from three different endpoints and inspect the response bodies. If the shapes differ, the API lacks a defined error contract. Error handling code in clients will need to branch based on which endpoint was called, creating maintenance burden.
The fourth check is idempotency. HTTP defines GET, HEAD, PUT, and DELETE as idempotent operations, meaning repeating the same request produces the same result. POST is not idempotent by definition. If your API uses POST for operations that should be idempotent, such as adding a tag that already exists, you either need to return 409 Conflict or make the operation naturally idempotent by using PUT instead.
Test your resource paths and methods using /tools/http-request-builder to quickly prototype requests and verify that status codes, Location headers, and response bodies match documented behavior before writing client integration code. Catching mismatches at the design stage is far cheaper than fixing them after clients have shipped.
Refactoring to clean REST resource design
Rename verb-based endpoints to noun-based resource paths incrementally, keeping the old paths alive with 301 redirects during the transition. A versioned API makes this easier: /v2/users can coexist with /v1/getAllUsers while clients migrate.
For status codes, audit every endpoint to confirm: POST that creates a resource returns 201 Created with a Location header pointing to the new resource URI. DELETE returns 204 No Content with an empty body. GET that finds nothing returns 404 Not Found. POST with invalid input returns 422 Unprocessable Entity with structured error details. Conflict on a unique constraint returns 409 Conflict. These mappings remove ambiguity from client code.
For error responses, define a single error schema and apply it across every endpoint. The minimal viable schema is { "error": { "code": string, "message": string, "details": array } }. Code is a machine-readable identifier like VALIDATION_ERROR or RESOURCE_NOT_FOUND. Message is a human-readable description. Details is an array of objects for field-level validation failures. Document this schema in your OpenAPI specification under components/schemas/ErrorResponse and reference it using dollar-ref in every endpoint's error response definitions.
For pagination, choose cursor-based pagination over offset-based for resources that receive frequent writes. Offset-based pagination with page=2 and limit=20 skips records when items are inserted before the page boundary, causing clients to miss items. Cursor-based pagination uses an opaque cursor value from the previous response to fetch the next page. The cursor encodes the position stably regardless of concurrent inserts.
For versioning, choose one strategy and apply it consistently. URI versioning with /v1/ and /v2/ prefixes is the most visible and easiest to test in browsers and curl. Header versioning with API-Version: 2024-01 keeps URLs stable but requires infrastructure that can route based on headers. Whatever strategy is chosen, document the deprecation timeline when old versions are retired.
Add X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers to every response so clients can implement proactive rate limit handling without waiting for a 429.
Partial updates, conflicts, and nested resources
PUT and PATCH are frequently confused. PUT replaces the entire resource at the given URI. If you PUT a user object and omit the phone field, the server should either store null for that field or return 400. PATCH applies partial changes, updating only the fields present in the request body. Using PUT when you intend a partial update silently deletes fields that clients did not include, which is a serious data loss bug in production.
Nested resources require careful design. /users/42/orders is a natural representation of orders belonging to a user, but it raises a question: can an order be accessed at /orders/99 as well? If yes, the same resource has two canonical paths, which creates cache invalidation complexity. A common solution is to use /users/42/orders for creation and listing in the user context, while /orders/99 is the canonical single-resource path.
Action endpoints for domain operations that do not map naturally to CRUD sometimes need special handling. /reports/99/publish is more readable than PUT /reports/99 with { status: "published" } in the body, especially when publishing involves side effects like sending notifications. The REST community calls these controller resources. They are acceptable when the operation has significant business meaning and cannot be expressed as a simple state change.
Idempotency keys are essential for any POST operation where duplicate processing causes harm. Payment endpoints, order creation, and inventory deductions all need idempotency guarantees. The client generates a UUID and includes it as Idempotency-Key in the request. The server stores the key and the result. Retrying with the same key returns the original result without re-processing.
Large collection endpoints need both pagination and filtering. Without filtering, clients must download thousands of records to find the few they need. Query parameters should follow a consistent pattern: filter[status]=active, sort=-createdAt (prefix minus for descending), page[cursor]=abc, page[limit]=20. Documenting this convention in the OpenAPI spec lets code generators produce correct client libraries automatically.
Design patterns that cause maintenance burden
Returning 200 OK for validation failures is the most common REST design mistake. It forces clients to parse the response body before they know whether the request succeeded, and it makes monitoring dashboards mislead because error rates appear as successes. Every 4xx status code has a precise meaning: 400 Bad Request for malformed syntax, 401 Unauthorized for missing authentication, 403 Forbidden for insufficient permissions, 404 Not Found for unknown resources, 409 Conflict for state conflicts, and 422 Unprocessable Entity for semantically invalid input.
Not returning a Location header on 201 responses forces clients to make a second request to discover the new resource URI. If the client already has the URI from the 201 Location header, it can display the created resource immediately without an additional round trip.
Using different error schemas across endpoints makes client error handling code significantly more complex. When the orders endpoint returns { msg: "error" } and the users endpoint returns { errors: [] }, every API call site needs special-case handling. Defining one error schema and enforcing it through a shared middleware eliminates this entirely.
Tunneling multiple operations through a single POST request is a step backwards toward RPC. An endpoint like POST /batch that accepts an array of operations with different types is harder to document, test, and rate limit than individual REST endpoints. Individual endpoints are preferable, with client-side orchestration for multi-step workflows.
Ignoring versioning from the start means the first breaking change requires a disruptive migration. Adding /v1/ to every path from day one, even before there is a v2, costs nothing and preserves flexibility. When a breaking change is needed, /v2/ can coexist with /v1/ while clients migrate at their own pace.
Not including rate limit headers in responses means clients have no signal that they are approaching a limit until they receive a 429. Adding X-RateLimit-Remaining and X-RateLimit-Reset to every response allows well-behaved clients to self-throttle before hitting the limit.
Building a consistent, maintainable REST API
Define your API contract in OpenAPI 3.1 before writing any server code. The specification forces you to think through resource paths, request schemas, response schemas, status codes, and error formats as a coherent system rather than discovering inconsistencies incrementally. Use the specification to generate server stubs, client SDKs, and documentation simultaneously.
Enforce the error response schema at the framework level rather than relying on individual endpoint authors. In Express, a centralized error middleware formats all errors into the standard shape before sending the response. In FastAPI, exception handlers do the same. When the error shape is enforced by infrastructure rather than convention, it cannot drift.
Write integration tests for every status code your API produces. Test that POST /v1/users returns 201 with a Location header. Test that GET /v1/users/9999 returns 404 with the standard error schema. Test that PUT /v1/users/42 with a missing required field returns 422. These tests serve as both a regression suite and living documentation.
Document idempotent behavior explicitly. When a DELETE endpoint is called twice, it should return 204 on the first call and 404 on the second. This is correct REST behavior but worth documenting so that clients can distinguish between a successful idempotent repeat and a genuine not-found error.
Add pagination, filtering, and sorting to collection endpoints before they are needed rather than after. An endpoint that initially returns all records becomes harder to paginate later because clients may depend on receiving all records in a single response. Designing for pagination from the start keeps future changes backward-compatible.
Use /tools/http-request-builder during design review to prototype new endpoints and verify that status codes, headers, and response shapes match the OpenAPI specification before implementation begins. Catching design inconsistencies before code is written saves the cost of both implementation and the client changes that would follow.
REST API design review checklist
- ✓All resource paths use nouns, not verbs; HTTP methods carry the action.
- ✓POST that creates a resource returns 201 Created with a Location header.
- ✓DELETE returns 204 No Content with an empty response body.
- ✓Error responses use a single consistent schema across every endpoint.
- ✓Validation failures return 422 Unprocessable Entity, not 200 with success: false.
- ✓Collection endpoints support cursor-based pagination, filtering, and sorting.
- ✓POST operations that must be safe to retry include Idempotency-Key support.
- ✓All responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers.
Related guides
Frequently asked questions
Why should REST API paths use nouns instead of verbs?
The HTTP method already expresses the action. GET, POST, PUT, PATCH, and DELETE each carry precise semantics defined by the HTTP specification. Adding a verb to the path duplicates that information and creates ambiguity: is GET /getUser different from GET /users/42? Noun-based paths like /users/42 let the HTTP method determine the operation, making the API consistent and predictable across every resource type.
What is the correct status code for a successful resource creation?
201 Created, along with a Location header that points to the URI of the newly created resource. This gives clients the canonical URI without requiring an additional request to discover it. 200 OK is incorrect for creation because it signals that an existing resource was retrieved, not that a new resource was created. Returning 200 for creates confuses monitoring dashboards and breaks cache invalidation logic.
What is the difference between PUT and PATCH?
PUT replaces the entire resource at the given URI. If a field is missing from the PUT body, the server stores null for that field, potentially overwriting existing data. PATCH updates only the fields present in the request body, leaving all other fields unchanged. Use PUT when you want the client to own the complete resource state and PATCH when you want a partial update without risk of silently clearing fields.
Why does offset-based pagination produce incorrect results?
Offset-based pagination uses LIMIT and OFFSET in the database query. When new items are inserted before the page boundary between page 1 and page 2 requests, the offset shifts, causing items to appear on two pages or disappear entirely. Cursor-based pagination uses an opaque cursor from the last item of the previous page, producing stable results regardless of concurrent inserts.
How should I format error responses to keep clients simple?
Use a single schema across every endpoint: { error: { code: string, message: string, details: array } }. Code is a machine-readable identifier like VALIDATION_ERROR. Message is a human-readable description. Details is an array of field-level validation problems. This structure allows client error handling code to be written once and applied uniformly regardless of which endpoint was called.
What is an idempotency key and when should I use it?
An idempotency key is a client-generated UUID sent in the Idempotency-Key request header. The server stores the key and the response. If the same key is sent again, the server returns the original response without reprocessing. Use idempotency keys for any POST operation where duplicate processing causes harm: payments, order creation, inventory deductions, and email sending are common examples.
Which API versioning strategy should I use?
URI versioning with a /v1/ prefix is the most practical choice for most APIs. It is visible in URLs, easy to test in browsers and curl, and straightforward to route in load balancers and gateways. Header-based versioning with API-Version: 2024-01 keeps URLs stable but requires infrastructure that routes on headers. Choose one strategy and apply it consistently across every endpoint from day one.
What rate limit headers should my REST API include?
Include X-RateLimit-Limit (the maximum requests allowed in the window), X-RateLimit-Remaining (requests left in the current window), and X-RateLimit-Reset (Unix timestamp when the window resets) in every response. These headers let well-behaved clients self-throttle before receiving a 429. When a 429 does occur, also include Retry-After to tell clients how long to wait.
All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-06.