How to Design a REST API: Best Practices and Examples
A practical guide to REST API design: resource naming, HTTP methods, status codes, error format, pagination and versioning, with sample endpoints you can copy.
Good REST API design models your data as plural noun resources (/orders, /orders/{id}), uses HTTP methods and status codes as the HTTP standard defines them, returns consistent JSON errors, paginates every list, and versions only for breaking changes. Write the contract in OpenAPI first, then build to it.
Photo: Unsplash
On this page
An API is a promise. Once other teams, partners or mobile apps start calling your endpoints, every naming choice and error format becomes hard to change. A rushed design turns into years of workarounds, support tickets and “v2” projects nobody wanted.
The good news is that REST API design is mostly about following a few well-established conventions consistently. This guide covers resource naming, HTTP methods, status codes, error responses, pagination and versioning, with sample endpoints for a simple online store you can adapt.
If you are new to the concept itself, start with what an API is, then come back here.
What makes an API “RESTful”
REST is an architectural style built on top of HTTP. In practice, a REST API means:
- Resources (orders, customers, products) each have a URL.
- HTTP methods (GET, POST, PUT, PATCH, DELETE) describe what you want to do with that resource.
- Requests are stateless. Each request carries everything the server needs, such as an auth token, rather than relying on a server-side session.
- Representations are usually JSON, and the response status code tells the client what happened.
Microsoft’s Azure Architecture Center guidance on RESTful API design sums up the goals as platform independence and loose coupling: any client should be able to call the API, and client and server should be able to evolve separately. Every practice below serves those 2 goals.
Name resources clearly
URLs should identify things, not actions. Use plural nouns and let the HTTP method express the verb.
Sample endpoints for an online store:
GET /v1/products List products
GET /v1/products/{productId} Get one product
POST /v1/orders Create an order
GET /v1/orders/{orderId} Get one order
PATCH /v1/orders/{orderId} Update part of an order
DELETE /v1/orders/{orderId} Cancel or delete an order
GET /v1/customers/{customerId}/orders List a customer's orders
Naming rules that save pain later:
- Use plural nouns:
/orders, not/orderor/getOrders. - Keep nesting shallow. One level (
/customers/{id}/orders) is fine. Deeper paths like/customers/{id}/orders/{id}/items/{id}/notesget brittle. Give deep objects their own top-level path if they have their own IDs. - Pick one case style and stick to it. Lowercase paths with hyphens (
/shipping-addresses) and camelCase or snake_case JSON fields are all common. Consistency matters more than the choice. - Use stable, opaque IDs. Avoid exposing database auto-increment IDs if you don’t want people guessing other records.
- Handle actions that are not CRUD with a sub-resource, such as
POST /v1/orders/{orderId}/refunds. That keeps the noun-based model and gives the action its own history.
Use HTTP methods the way HTTP defines them
The HTTP standard (RFC 9110, HTTP Semantics) defines which methods are “safe” (they don’t change state) and which are “idempotent” (repeating the request has the same effect as sending it once). Clients, proxies and retry logic rely on those properties, so honor them.
| Method | Typical use | Safe | Idempotent | Typical success code |
|---|---|---|---|---|
| GET | Read a resource or list | Yes | Yes | 200 OK |
| POST | Create a resource or trigger an action | No | No | 201 Created (with Location header) or 202 Accepted |
| PUT | Replace a resource fully | No | Yes | 200 OK or 204 No Content |
| PATCH | Update part of a resource | No | Not guaranteed | 200 OK |
| DELETE | Remove a resource | No | Yes | 204 No Content |
2 common mistakes to avoid:
- Changing data with GET. Crawlers, link previews and caches all assume GET is safe. A
GET /orders/123/cancelendpoint will eventually cancel orders nobody meant to cancel. - Unsafe retries on POST. Because POST is not idempotent, a network timeout can lead to duplicate orders or payments. Many payment APIs solve this with an idempotency key header that the client generates and the server remembers for a time window. If your API creates money-moving or inventory-changing records, consider the same pattern.
Return the right status codes
Status codes are the first thing a client checks, so use them precisely. You don’t need dozens. A small, consistent set covers most APIs:
- 200 OK: successful read or update.
- 201 Created: a new resource was created. Return its URL in the
Locationheader. - 204 No Content: success with no body, common for DELETE.
- 400 Bad Request: malformed request or failed validation.
- 401 Unauthorized: missing or invalid credentials.
- 403 Forbidden: authenticated, but not allowed.
- 404 Not Found: the resource does not exist (or the caller cannot know it exists).
- 409 Conflict: the request clashes with current state, such as a duplicate or a version mismatch.
- 422 Unprocessable Content: syntactically valid but semantically wrong input. Some teams use 400 for all validation errors instead, which is also fine if consistent.
- 429 Too Many Requests: the client hit a rate limit. Include a
Retry-Afterheader. - 500 and 503: server-side errors. Never return 200 with an error message in the body.
Make errors consistent and useful
Nothing frustrates API users more than every endpoint inventing its own error shape. Pick one format and use it everywhere.
A good default is the IETF’s Problem Details for HTTP APIs standard, RFC 9457, published in July 2023 to replace RFC 7807. It defines a JSON object served with the application/problem+json media type:
json
{
"type": "https://api.example.com/problems/insufficient-stock",
"title": "Not enough stock",
"status": 409,
"detail": "Only 2 units of SKU 4471 are available; 5 were requested.",
"instance": "/v1/orders/8f3c2a",
"errors": [
{ "field": "items[0].quantity", "message": "Must be 2 or fewer" }
]
}
Tips for error responses:
- Give each error type a stable, documented identifier so clients can handle it in code.
- Put human-readable detail in
detail, but never leak stack traces, SQL or internal hostnames. The OWASP Top 10 lists security misconfiguration among the most common web risks. - For validation errors, list every failing field at once rather than making the client fix them one by one.
- Include a request or trace ID so support teams can find the matching logs.
Key takeaway: Consistency beats cleverness. One naming style, one error format and one pagination pattern across every endpoint will save your API consumers more time than any individual “perfect” design choice.
Paginate every list from day one
Any endpoint that returns a list will eventually return too many items. Add pagination before launch, because adding it later is a breaking change for clients that expect the full list.
| Pattern | Example request | Pros | Cons |
|---|---|---|---|
| Offset and limit | GET /v1/orders?offset=40&limit=20 |
Simple, supports jumping to page N | Slow on large tables; items shift when data changes |
| Page number | GET /v1/orders?page=3&pageSize=20 |
Easy for UIs | Same drift problems as offset |
| Cursor (token) | GET /v1/orders?pageToken=eyJpZCI6...&pageSize=20 |
Stable and fast on large or changing data | Cannot jump to an arbitrary page |
For most new APIs, cursor-based pagination is the safer default. Google’s API Improvement Proposal on pagination (AIP-158) describes a widely copied version: the request accepts a page size and an opaque page token, the response returns a next_page_token, and an empty token means the end of the list. Keep the token opaque so you can change how it works internally.
A sample response:
json
{
"data": [ { "id": "ord_101" }, { "id": "ord_102" } ],
"nextPageToken": "c2Vla19hZnRlcl9vcmRfMTAy"
}
Also set a sensible default and a maximum page size, and pair pagination with filtering and sorting parameters such as ?status=shipped&sort=-createdAt.
Version only for breaking changes
Versioning lets you change your API without breaking existing clients. The 3 common approaches:
- URL path:
/v1/orders. The most visible and the easiest to route and cache. Most public APIs use it. - Header: a custom header or media type parameter. Keeps URLs clean but is harder to test in a browser.
- Query parameter:
?api-version=2026-10-01. Used by some large cloud platforms, often with date-based versions.
Whichever you choose, the bigger discipline is avoiding breaking changes in the first place:
- Non-breaking (safe in the same version): adding a new endpoint, adding an optional field, adding a new optional query parameter.
- Breaking (needs a new version): removing or renaming a field, changing a field’s type, making an optional field required, changing error codes clients depend on.
Tell clients to ignore unknown fields, publish a deprecation timeline, and keep the old version running long enough for consumers to move.
Security and performance basics
Design these in rather than bolting them on:
- HTTPS only, with authentication via OAuth 2.0 or scoped API keys. Check authorization on every object, not just every endpoint, so users cannot read other customers’ orders by changing an ID.
- Rate limiting with clear 429 responses.
- Input validation against your schema on every request.
- Caching headers such as
ETagandCache-Controlon GET responses, andIf-Matchfor safe concurrent updates. - Event notifications for long-running work. Instead of making clients poll, send webhooks or publish events, a pattern explained in our guide to event-driven architecture.
Where to start: a step-by-step design process
- List the use cases. Write down what clients need to do, in plain language (“a customer views past orders”).
- Model the resources. Turn nouns into resources and decide their relationships and IDs.
- Write the contract first in OpenAPI. The OpenAPI Specification, maintained by the OpenAPI Initiative (version 3.2.0 was released in September 2025), lets you describe endpoints, schemas and errors in a file that generates docs, mocks and client SDKs.
- Review it with real consumers. Have a frontend or partner developer try the mock before you write backend code.
- Pick your conventions and write them down. Naming, errors, pagination, dates (use ISO 8601 in UTC) and versioning in a 1-page style guide.
- Build, test and lint the contract. Use contract tests in your CI/CD pipeline so the implementation never drifts from the spec.
- Publish docs and a changelog. Treat the API as a product with its own release notes.
Not sure REST is the right fit at all? Our comparison of REST vs GraphQL covers when a query-based API makes more sense.
Conclusion
Strong REST API design comes down to predictable patterns: noun-based resources, correct HTTP methods and status codes, one error format, pagination on every list and versioning reserved for real breaking changes. Write the contract before the code, get feedback early, and document your conventions so every new endpoint feels familiar. Your future self, and every developer who calls your API, will thank you.
Sources
- RFC 9110: HTTP Semantics (IETF) — definitions of HTTP methods, safe and idempotent properties, and status codes
- RFC 9457: Problem Details for HTTP APIs (IETF) — standard JSON error format, replaces RFC 7807
- Microsoft Learn: Best practices for RESTful web API design — resource modeling, loose coupling, pagination and versioning guidance
- Google AIP-158: Pagination — page token and next page token pattern
- OpenAPI Specification v3.2.0 (OpenAPI Initiative) — standard for describing REST APIs
Frequently asked questions
What are the most important REST API design best practices?
The most important practices are using plural noun resources, applying HTTP methods and status codes correctly, returning one consistent error format, paginating every list, and versioning only for breaking changes. Add HTTPS, authentication and rate limiting from the start. Writing the contract first in OpenAPI helps you catch design problems before any code exists.
Should REST API endpoints use nouns or verbs?
Use nouns. The URL identifies the resource, such as /orders/123, and the HTTP method supplies the verb: GET to read, POST to create, PATCH to update, DELETE to remove. For actions that don't fit create, read, update or delete, model the action as a sub-resource, such as POST /orders/123/refunds, instead of a verb path like /cancelOrder.
What is the best way to version a REST API?
URL path versioning, such as /v1/orders, is the most common and easiest for clients to understand and test. Header and query parameter versioning also work. The more important rule is to avoid breaking changes: add fields and endpoints without bumping the version, and create a new version only when you must remove, rename or change the type of something.
What is the difference between PUT and PATCH?
PUT replaces the entire resource with the representation you send, so missing fields may be cleared. PATCH applies a partial update to only the fields you include. Under the HTTP standard, PUT is idempotent, meaning repeating it has the same effect as sending it once, while PATCH is not guaranteed to be. Many APIs offer PATCH for everyday edits.
How should a REST API return errors?
Return an accurate HTTP status code plus a consistent JSON body. A good default is RFC 9457 Problem Details, which uses fields like type, title, status, detail and instance with the application/problem+json media type. List all validation errors at once, include a request ID for support, and never expose stack traces or internal system details.
What is cursor-based pagination?
Cursor-based pagination returns a page of results plus an opaque token that points to where the next page starts. The client sends that token back to get the next page, and an empty token means there are no more results. It stays fast and accurate on large or frequently changing data, unlike offset pagination, but it cannot jump straight to page 50.
Do I need OpenAPI to design a REST API?
You don't strictly need it, but it is strongly recommended. An OpenAPI file is a machine-readable contract that generates documentation, mock servers, client SDKs and contract tests. Designing in OpenAPI first lets consumers review and try the API before you build it, which is far cheaper than changing a live API.
Was this guide useful
No reviews yet. Share your experience and help other readers.