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.

Beginner 6 min read · Updated

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.

JSON over HTTPSvalidatedcheck keywriteClientweb, mobile, partnerAPI gatewayauth, rate limitsOrders APIPOST /v1/ordersIdempotency keysseen requestsOrders DB
A request's path through a well-designed API: one versioned contract at the edge, an idempotency key so retries are safe, and storage hidden behind the service.

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:

RequestMeaning
GET /usersList users
POST /usersCreate 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:

OperationRequestSuccess response
List a user's ordersGET /v1/users/{userId}/orders?status=shipped&limit=20&cursor=…200 with items and nextCursor
Get one orderGET /v1/orders/{orderId}200 with the order
Create an orderPOST /v1/orders with an Idempotency-Key header201 with the order and a Location header
Update part of an orderPATCH /v1/orders/{orderId}200 with the updated order
Cancel an order (an action)POST /v1/orders/{orderId}/cancel200, or 202 if asynchronous
Delete an addressDELETE /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 /books

A 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 exist

Status 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_chronological

The 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_token and previous_token: opaque cursors the client sends back to get the next or previous page. newest_id lets the client ask for anything newer than what it already shows, which handles real-time refresh.
  • Versioning: the /2/ in api.twitter.com/2 is the API version, so the API can change in the future without breaking clients built against version 1.

Get mentions

GET /2/users/:id/mentions

Returns 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

MethodSafeIdempotentUse
GETYesYesRead; never change state
PUTNoYesReplace a resource at a known URL
PATCHNoNot necessarilyPartial update
DELETENoYesRemove
POSTNoNo (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 429 and Retry-After. See rate limiting.
  • Optimistic concurrency with versions or ETags (If-Match), returning 409 or 412 on conflicting updates. See optimistic locking.
  • Asynchronous operations: return 202 Accepted with 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

Public developer APIs

Versioned, documented endpoints with idempotency keys, rate limits and consistent errors, like the Twitter, Stripe and GitHub APIs.

Mobile backends

Compact, paginated responses and long backward-compatibility windows for old app versions.

Internal microservices

Consistent conventions across teams make services easy to call and debug.

Interview designs

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