API Design Best Practices: Resources, Versioning and Errors
REST API design best practices for system design: resource naming, status codes, versioning, pagination, idempotency, errors and backward compatibility.
API design is deciding the contract between a service and its clients: the resources and operations it exposes, how requests and responses are shaped, how errors are reported, and how the contract evolves without breaking anyone. A well-designed API is predictable, hard to misuse, efficient over the network and stable for years.
It comes up constantly in interviews, especially at junior and mid levels: you will usually be asked to design a RESTful API for a scenario. GraphQL and gRPC are growing (see REST vs gRPC vs GraphQL), but most interview questions still expect REST. Writing the key endpoints early also clarifies requirements and anchors the rest of the design.
What makes an API RESTful
REST is a set of conventions for building APIs on top of HTTP. It uses standard HTTP methods and resource-based URLs. Three principles do most of the work:
- Statelessness: every request carries everything needed to process it (including credentials), and the server keeps no session between requests. Any server can handle any request, which is what makes REST services easy to scale horizontally.
- Uniform interface: resources are reached the same way everywhere, through URIs, with the same methods meaning the same things.
- Resource-based architecture: everything is a resource (users, posts, comments), and resources are manipulated with standard HTTP methods.
The classic set of endpoints for one resource:
| Request | Meaning |
|---|---|
GET /users | List users |
POST /users | Create a user |
GET /users/{id} | Get one user |
PUT /users/{id} | Replace (update) that user |
PATCH /users/{id} | Update some fields of that user |
DELETE /users/{id} | Delete that user |
Resource-oriented design in practice
A fuller example for an online store:
| Operation | Request | Success response |
|---|---|---|
| List a user's orders | GET /v1/users/{userId}/orders?status=shipped&limit=20&cursor=… | 200 with items and nextCursor |
| Get one order | GET /v1/orders/{orderId} | 200 with the order |
| Create an order | POST /v1/orders with an Idempotency-Key header | 201 with the order and a Location header |
| Update part of an order | PATCH /v1/orders/{orderId} | 200 with the updated order |
| Cancel an order (an action) | POST /v1/orders/{orderId}/cancel | 200, or 202 if asynchronous |
| Delete an address | DELETE /v1/addresses/{addressId} | 204 with no body |
- Use plural nouns and stable, opaque IDs in paths; avoid verbs except for actions that don't map cleanly to CRUD (like
cancel). - Nest resources only one level deep where ownership is clear; otherwise use filters (
/orders?userId=…). - Keep naming consistent: one casing style, the same field names across endpoints, ISO 8601 timestamps, and money as integers in minor units with a currency code.
Five common mistakes, and the fix
REST design is mostly straightforward, but the same mistakes appear again and again.
1. Verbs in URLs
Wrong: GET /getAllBooks
POST /createNewBook
Right: GET /books
POST /booksA URL should name a resource (a noun), not an action. The HTTP method already says what to do with it.
2. The wrong HTTP method
Wrong: POST /books/123/update
Right: PUT /books/123 (or PATCH for a partial update)Each method has one job: GET reads, POST creates, PUT replaces, PATCH partially updates, DELETE removes. Using them as intended makes the API guessable, and lets caches, proxies and retry logic behave correctly.
3. One status code for everything
Wrong: 200 OK for every response, with errors described in the body
Right: 200 OK successful GET, PUT or PATCH
201 Created successful POST that created something
204 No Content successful DELETE
404 Not Found the resource doesn't existStatus codes are a standard vocabulary for the outcome of a request. Used properly, clients can react without parsing the body: retry on 503, re-authenticate on 401, fix the input on 400.
4. Inconsistent naming
Wrong: /book/123 and /authors (singular and plural mixed)
Right: /books/123 and /authors (always plural)Consistency lets developers predict endpoints they haven't seen, which means fewer mistakes and less time in the docs.
5. No pagination
Wrong: return every record in one response
Right: GET /books?limit=50&cursor=… (or ?page=2&limit=50)Returning everything is slow, memory-hungry and gets worse as data grows. Split lists into pages. See pagination for offset vs cursor strategies.
Worked example: Twitter's timeline API
Let's apply this to a real API: the home timeline of Twitter (X). It shows tweets from people the user follows, plus retweets and mentions. The design has to handle:
- Data format: a response that is easy to parse.
- Pagination: a timeline is effectively endless.
- Real-time updates: fetching tweets newer than the ones on screen.
- Mentions: tweets in which the user is mentioned.
Get a user's timeline
GET https://api.twitter.com/2/users/:id/timelines/reverse_chronologicalThe response is a JSON object with two parts: data, an array of tweets, and meta, information about the page, including the tokens used to fetch more. A trimmed sample:
{
"data": [
{
"id": "1524796546306478083",
"edit_history_tweet_ids": ["1524796546306478083"],
"text": "Today marks the launch of a new video series for developers…"
},
{
"id": "1524468552404668416",
"edit_history_tweet_ids": ["1524468552404668416"],
"text": "Join us tomorrow as we discuss the future of bots…"
}
],
"meta": {
"result_count": 2,
"newest_id": "1524796546306478083",
"oldest_id": "1524468552404668416",
"next_token": "7140dibdnow9c7btw421dyz6jism75z99gyxd8egarsc4",
"previous_token": "7140dibdnow9c7btw421dyz6jism75z99gyxd8egarsc4"
}
}- `edit_history_tweet_ids` lists the IDs of every version of a tweet, added when Twitter introduced editing. A good example of an additive, non-breaking change.
- Pagination uses
next_tokenandprevious_token: opaque cursors the client sends back to get the next or previous page.newest_idlets the client ask for anything newer than what it already shows, which handles real-time refresh. - Versioning: the
/2/inapi.twitter.com/2is the API version, so the API can change in the future without breaking clients built against version 1.
Get mentions
GET /2/users/:id/mentionsReturns tweets that mention the given user, the 10 most recent by default, and up to the most recent 800 by paging. The response has the same data and meta shape, with the same token-based pagination and the same versioning, so a client that can read one endpoint can read the other. That consistency is the point.
Methods, status codes and safety
| Method | Safe | Idempotent | Use |
|---|---|---|---|
| GET | Yes | Yes | Read; never change state |
| PUT | No | Yes | Replace a resource at a known URL |
| PATCH | No | Not necessarily | Partial update |
| DELETE | No | Yes | Remove |
| POST | No | No (make it so with idempotency keys) | Create, or trigger actions |
Safe means the request changes nothing; idempotent means repeating it has the same effect as doing it once, so it can be retried safely. Use status codes for their meaning: 400 for invalid input, 401 unauthenticated, 403 forbidden, 404 not found, 409 conflict (such as a version mismatch), 422 validation failure, 429 rate limited, 5xx server faults. Clients and proxies rely on these to decide whether to retry. See HTTP.
Errors clients can act on
HTTP/1.1 422 Unprocessable Entity
{
"error": {
"code": "INSUFFICIENT_STOCK",
"message": "Only 2 units of this item are available.",
"details": [{ "field": "items[0].quantity", "available": 2 }],
"requestId": "req_8f2c..."
}
}Give a stable, machine-readable code, a human-readable message, field-level details for validation, and a request ID for support. Never leak stack traces or internal hostnames.
Designing for scale and safety
- Pagination on every list, preferably cursor-based. See pagination.
- Idempotency keys on creates and payments, so clients can retry safely after timeouts. See idempotency.
- Authentication with API keys, sessions or JWTs, and authorization on every resource. See authentication and authorization.
- Rate limits per client, with
429andRetry-After. See rate limiting. - Optimistic concurrency with versions or ETags (
If-Match), returning409or412on conflicting updates. See optimistic locking. - Asynchronous operations: return
202 Acceptedwith a status URL for long-running work, or notify via webhooks. - Field selection and compression to keep payloads small on mobile networks.
Versioning and compatibility
- Additive changes are safe: new optional fields, new endpoints, new enum values if clients are told to ignore unknown ones.
- Breaking changes need a new version: removing or renaming fields, changing types or meanings, or making optional inputs required.
- Version explicitly, in the path (
/v1/) or a header, and run old and new versions side by side with a published deprecation timeline. - Be tolerant in what you accept from older clients, and never reuse a field name for a different meaning.
- Mobile clients linger for months, so compatibility windows must be long. See mobile release engineering.
In an interview, write three to five key endpoints with their request and response fields right after requirements: one per functional requirement, nouns in the path, the right method, the main fields. It forces decisions about IDs, pagination and what the client actually needs, and interviewers notice.
Where it’s used
Versioned, documented endpoints with idempotency keys, rate limits and consistent errors, like the Twitter, Stripe and GitHub APIs.
Compact, paginated responses and long backward-compatibility windows for old app versions.
Consistent conventions across teams make services easy to call and debug.
Every design problem includes an API step: see the endpoints in Design Twitter and Design Pastebin.
In an interview
- Write one endpoint per functional requirement, with method, path and main fields.
- Nouns in paths, plural and consistent; let the HTTP method be the verb.
- Use correct status codes and a structured error format.
- Add pagination, idempotency keys and rate limits by default, and say how the API is versioned.
Further reading
- Microsoft REST API GuidelinesMicrosoft, GitHub
- Google API Design GuideGoogle Cloud