Back to Blog
Backend12 min readJun 2026

REST API Design: Clean, Predictable HTTP APIs

Resources as nouns, the right verbs and status codes, sane pagination, and error shapes that don't lie. A field guide to APIs other engineers actually enjoy using.

BackendAPIRESTDesign
SB

Sri Balaji

Founder · TheSimplifiedTech

On this page

Why most APIs are annoying to use

You integrate with an API and within ten minutes you're confused. The endpoint to delete a thing is POST /deleteUser. Creating a record returns 200 OK even when validation failed. Listing 50,000 rows dumps all of them in one response. The error body is a raw stack trace. None of this is hard to fix, it's just that nobody decided on the rules up front.

A good HTTP API is predictable: once you've seen three endpoints, you can guess the fourth. That predictability is not an accident, it comes from a small set of conventions that REST gives you for free. This article walks through those conventions: how to name resources, pick verbs and status codes, shape requests and responses, paginate, filter, and report errors, plus when REST is the wrong tool and you should reach for GraphQL or gRPC instead.

Who this is for

Backend engineers designing their first public or internal API, and anyone who has wired up a few endpoints but never stepped back to ask whether the shape is right. You should be comfortable with HTTP basics, if requests and responses feel fuzzy, read [How the Web Works](/blog/how-the-web-works-http-requests) first.

The one principle everything follows from

Model your API around resources, the nouns of your system, and let HTTP's verbs and status codes describe what happens to them. The protocol already has the vocabulary; your job is to use it consistently.

Think of a well-organized public library. Every book has a fixed, predictable address: a shelf, a section, a call number. You don't ask the librarian to runBookFetchProcedure, you say "I want the book at this location," and the action (borrow, return, reserve) is a separate, standard gesture. The catalog is the same for every book, so once you understand one, you understand all of them. A REST API is that catalog: stable addresses for things, standard actions on them.

A book's shelf location / call numberA resource URL, /users/42
Borrow, return, reserve (standard gestures)HTTP verbs, GET, POST, PUT, DELETE
The catalog card, identical for every bookA consistent JSON response shape
"Not on shelf" vs "no such book"Status codes, 409 vs 404
Section → shelf → bookNested resources, /users/{id}/orders
REST borrows a vocabulary you already understand from a well-run library.

The resource model, in one picture

Before you write a single handler, sketch the resources and how they nest. A collection (/users) holds items, each item has a stable address (/users/{id}), and items can own sub-collections (/users/{id}/orders). That hierarchy is your whole URL design.

GET by idownsGET by id
/users

Collection of users

/users/{id}

A single user

/users/{id}/orders

That user's orders

/users/{id}/orders/{orderId}

A single order

Collections, items, and nested sub-collections form a predictable tree.

Notice what's missing: there are no verbs in the path. /users/{id}/orders describes a thing, "the orders belonging to this user", and the HTTP method decides whether you're reading the list (GET), adding one (POST), and so on. The URL is a noun; the method is the verb.

Designing one endpoint, step by step

  1. 1

    Name the resource as a plural noun

    It's a collection of things, so use the plural: /orders, not /order or /getOrders. Plurals read naturally both for the list (/orders) and the item (/orders/123).

  2. 2

    Pick the verb from the action

    Reading is GET, creating is POST, full replace is PUT, partial update is PATCH, removal is DELETE. The verb never goes in the URL.

  3. 3

    Decide the request shape

    GET carries no body, filters go in the query string. POST/PUT/PATCH take a JSON body with only the fields the client controls (never let them set id, createdAt, or owner).

  4. 4

    Choose the success status code

    200 for a successful read or update, 201 Created for a new resource (with a Location header), 204 No Content for a delete with nothing to return.

  5. 5

    Define the response shape

    Return the resource (or a list wrapped with pagination metadata). Keep the shape identical to what the matching GET returns, so clients learn it once.

  6. 6

    Map the failure modes to codes

    400 for malformed input, 401/403 for auth, 404 for a missing resource, 409 for a conflict, 422 for validation. Each gets the same structured error body.

Verbs and status codes: the lookup table

Most of REST collapses into one table. Memorize this and you've covered ninety percent of the endpoints you'll ever build. The pattern repeats for every resource in your system, that repetition is exactly what makes the API predictable.

OperationMethod + PathSuccess status
List users (paginated)`GET /users``200 OK`
Get one user`GET /users/{id}``200 OK` / `404`
Create a user`POST /users``201 Created`
Replace a user`PUT /users/{id}``200 OK`
Update some fields`PATCH /users/{id}``200 OK`
Delete a user`DELETE /users/{id}``204 No Content`
The canonical operations on a users collection. Every other resource looks the same.

Idempotency matters

GET, PUT, and DELETE are **idempotent**, calling them twice has the same effect as calling them once. POST is not, which is why a retried POST can create duplicates. Knowing this tells clients which requests are safe to retry on a flaky network.

Request and response shapes that scale

The hardest part of a list endpoint isn't the happy path, it's everything around the data. A real GET /users needs pagination (don't return 50,000 rows), filtering (let callers narrow results without new endpoints), and a consistent error shape for when things go wrong. Here's a single well-designed endpoint showing all three.

GET /users?status=active&page=2&limit=20
json
// Request: filters and paging live in the query string, never the path.
//   GET /users?status=active&sort=-createdAt&page=2&limit=20
//
// Response: 200 OK
{
  "data": [
    { "id": "u_8a31", "name": "Mara Voss", "status": "active", "createdAt": "2026-05-02T10:14:00Z" },
    { "id": "u_8a32", "name": "Theo Lund", "status": "active", "createdAt": "2026-05-02T09:51:00Z" }
  ],
  "pagination": {
    "page": 2,
    "limit": 20,
    "totalItems": 4213,
    "totalPages": 211,
    "nextPage": "/users?status=active&sort=-createdAt&page=3&limit=20"
  }
}

Three things to copy here. The actual records live under a data key, never at the top level, that leaves room to add pagination (and later, things like meta) without breaking clients. Filters and sort are query parameters, so you never invent a new endpoint for "active users sorted by date." And the response hands back a ready-made nextPage link so the client doesn't have to build URLs by hand.

Now the failure case. When validation fails, return a status code that says "you" (a 4xx), not "me" (a 5xx), and a structured body the client can actually parse, not a sentence and definitely not a stack trace.

POST /users, validation failed
json
// Response: 422 Unprocessable Entity
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "requestId": "req_b7c2f0",
    "details": [
      { "field": "email", "issue": "must be a valid email address" },
      { "field": "age", "issue": "must be greater than 0" }
    ]
  }
}

A machine-readable code lets clients branch (if (error.code === "validation_failed")), a human message helps the developer debugging it, a requestId ties the failure to your server logs, and per-field details let a frontend highlight the exact inputs that were wrong. Use this shape for every error in the API, from 400 to 503, so callers write their error handling once.

When REST is the wrong tool

REST is the right default for most resource-shaped, public-facing APIs. But two situations push you elsewhere: clients that need wildly different slices of data (a mobile screen wants 3 fields, a dashboard wants 30), and high-throughput internal service-to-service calls where every millisecond and byte counts.

DimensionRESTGraphQLgRPC
Best forPublic/CRUD APIsVaried client data needsInternal service-to-service
Data fetchingFixed per endpointClient picks exact fieldsFixed, typed RPC methods
Transport / formatHTTP + JSONHTTP + JSONHTTP/2 + Protobuf (binary)
CachingEasy (HTTP caching)Harder (POST queries)Custom
Learning curveLowMediumMedium–high
Pick the style that matches the consumer and the traffic.

Rule of thumb

Start with REST. Reach for **GraphQL** when many different clients each need a different shape of the same graph of data and over-/under-fetching is real pain. Reach for **gRPC** when two of your own services talk to each other at high volume and you want strict typed contracts and a binary wire format.

Common mistakes that make APIs painful

  1. Verbs in the URL. POST /createUser, GET /getUserById, POST /users/123/delete. The method already is the verb, use POST /users, GET /users/123, DELETE /users/123.
  2. Returning 200 for errors. A 200 OK wrapping { "success": false } forces every client to parse the body to know if the call worked, and breaks monitoring that watches status codes. Let the status code tell the truth.
  3. No pagination on list endpoints. Returning every row works in dev with 12 records and falls over in prod with 12 million. Paginate from day one, adding it later is a breaking change.
  4. Leaking internals. Stack traces, SQL fragments, ORM class names, and internal IDs in responses are a security and coupling hazard. Return a clean structured error with a requestId, and keep the gory details in your logs.
  5. Inconsistent shapes. data here, results there, items somewhere else; snake_case in one endpoint, camelCase in another. Pick one convention and enforce it everywhere, consistency is the whole product.

Takeaways

REST API design in eight lines

  • Model resources as plural nouns; never put verbs in the URL.
  • Let HTTP methods be the verbs: GET, POST, PUT, PATCH, DELETE.
  • Status codes must tell the truth, 2xx for success, 4xx for the client, 5xx for you.
  • Use 201 + Location on create, 204 on delete, 422 for validation.
  • Wrap lists in a `data` key with `pagination` metadata, paginate from day one.
  • Put filters and sorting in the query string, not new endpoints.
  • Use one structured error shape everywhere, with a `code` and a `requestId`.
  • Default to REST; reach for GraphQL or gRPC only when the consumer demands it.

Where to go next

You now have the shape of a clean API. The next questions are how it changes over time and how you keep it safe, both are their own disciplines.

Want to go deeper?

This article covers concepts taught hands-on in the Cloud Engineer and DevOps career paths, with real terminal labs, production scenarios, and structured lessons.