Part 5 · APIs & Communication

REST API Design Best Practices

An API is a contract other teams depend on for years. How to design REST APIs that are predictable: nouns and HTTP methods, honest status codes, one error shape, filtering and pagination, free HTTP features, security basics and OpenAPI.

Lesson 3012 min readbeginner#api#rest

On this page

  1. The Problem
  2. The Core Idea
  3. How It Works
  4. Trade-offs
  5. In the Real World
  6. Interview Questions
  7. Key Takeaways
  8. Further Reading

The Problem

An API (Application Programming Interface) is a set of rules that lets one program ask another program for data or actions. A web API works over HTTP, so a browser or phone app can call a server. Here are two web APIs for the same feature.

API A:

HTTP
POST /getUserOrders        {"userId": 42}POST /createNewOrder       {...}POST /deleteOrderById      {"id": 7}→ always 200 OK, with {"success": false, "error": "not found"} when things fail

API B:

HTTP
GET    /users/42/ordersPOST   /ordersDELETE /orders/7→ 200, 201, 404, 409 … with clear error bodies

Both "work". But API B is predictable. A developer who has used any good API can guess how it behaves. Caches, load balancers and monitoring tools understand it. Clients can retry safely.

API A forces everyone to read the docs for every endpoint. It also hides failures from every tool in between. (An endpoint is one URL plus method that the API offers.)

An API is a contract that other teams, mobile apps and partners depend on for years. Good design saves everyone time. Bad design is very hard to fix later.

The Core Idea

Think of a well-organised library. HTTP methods are the standard actions of the web, such as GET to read and POST to create.

  • Everything is a thing with an address: /books/1984, /members/42, /members/42/loans.
  • You use a small set of standard actions on those things: look at (GET), add (POST), replace (PUT), change part of (PATCH), remove (DELETE).
  • The librarian answers with standard responses: "Here it is" (200), "Added" (201), "We don't have that" (404), "You're not a member" (401).

REST (Representational State Transfer) is a style for designing APIs. You build the API around resources (things, named with nouns) and use standard HTTP methods (actions, the verbs). You use HTTP the way it was designed to be used.

How It Works

REST's core principles (in plain words)

Roy Fielding described REST in his 2000 PhD dissertation. REST is a set of design rules, not a product. The main ideas:

  • Client–server: the client (frontend) and the server (backend) are separate. Each can change on its own.
  • Stateless: each request carries everything the server needs (like an auth token). The server does not remember earlier requests. This makes horizontal scaling easy, because any server can answer any request (lesson 7).
  • Cacheable: responses say whether a copy can be saved and reused (a cache). See lessons 14 to 16.
  • Uniform interface: every API looks alike. Resources have URLs, you use standard methods, and data comes in a standard format (usually JSON, a text format for data).
  • Layered system: the client does not need to know if there are proxies, gateways or CDNs in between (lesson 13).

1. Model resources with nouns

✅ Good
  • +GET /products
  • +GET /products/991
  • +POST /products
  • +PATCH /products/991
  • +DELETE /products/991
❌ Avoid
  • −GET /getAllProducts
  • −POST /getProduct
  • −POST /updateProductPrice
  • −GET /deleteProduct?id=991 — GET must never change data
Resources are nouns; the HTTP method is the verb. · compare

Rules of thumb:

  • Use plural nouns for collections: /users, /orders.
  • Use IDs for single items: /orders/5521.
  • Nest to show ownership, but only one or two levels: /users/42/orders ✅. /users/42/orders/5521/items/3/reviews ❌ is too deep. Use /order-items/3/reviews or a filter instead.
  • Use lowercase, hyphen-separated paths: /shipping-addresses.

What about actions that are not CRUD? CRUD means Create, Read, Update, Delete. "Cancel an order" or "send a password reset" do not fit these methods well. Two common approaches:

HTTP
POST /orders/5521/cancel      (an action sub-resource: pragmatic and common)POST /password-resets         (model the action as creating a resource)

2. Use HTTP methods correctly

Idempotent means that doing the same request many times has the same effect as doing it once. It matters because a client can safely retry an idempotent request after a network error. Example: deleting order 7 twice still leaves order 7 deleted.

MethodUseIdempotent?Typical success code
GETRead a resource or list✅ Yes200
POSTCreate, or trigger an action❌ No201 (created) / 200 / 202 (accepted for later)
PUTReplace a resource completely✅ Yes200 / 204
PATCHChange some fieldsUsually treated as ❌200 / 204
DELETERemove✅ Yes204 / 200

Never change data with GET. Browsers, crawlers and caches assume GET is safe and may call it at any time.

3. Return meaningful status codes

A status code is a number in the HTTP response that tells the client what happened. (See lesson 3 for the full list.) These are the ones every API should use correctly:

HTTP
200 OK             – success with a body201 Created        – new resource; Location: /orders/5521202 Accepted       – will be processed later (async)204 No Content     – success, nothing to return400 Bad Request    – malformed input401 Unauthorized   – not logged in / bad token403 Forbidden      – logged in, but not allowed404 Not Found      – no such resource (or hidden for privacy)409 Conflict       – clashes with current state422 Unprocessable  – valid JSON, fails business validation429 Too Many Req.  – rate limited; include Retry-After500 / 503          – server-side problems

4. Consistent error responses

Every error should have the same shape, with a code that programs can read and a message that people can read. There is a standard for this called Problem Details (RFC 9457, a document that defines an error format for HTTP APIs):

JavaScript
HTTP/1.1 422 Unprocessable ContentContent-Type: application/problem+json{  "type": "https://api.shop.com/errors/insufficient-stock",  "title": "Insufficient stock",  "status": 422,  "detail": "Only 2 units of product 991 are available.",  "instance": "/orders",  "errors": [{ "field": "items[0].quantity", "message": "max 2" }]}
  • Clients (the programs that call the API) can choose what to do based on the type (or a code field).
  • Humans can read detail.
  • Never show stack traces (the list of code lines where an error happened) or other internal details in production errors.

5. Filtering, sorting, searching and field selection

A query parameter is a name=value pair after the ? in a URL. Use query parameters on collections:

HTTP
GET /products?category=shoes&brand=nike&min_price=1000&max_price=5000GET /products?sort=-created_at            (minus = descending)GET /products?q=running+shoes             (search)GET /products?fields=id,name,price        (only these fields)GET /orders?status=pending&created_after=2027-01-01

Lists should always be paginated. Pagination means sending the data in small pages. Never return "all orders" at once (covered in lesson 34).

6. Design request and response bodies well

  • Use JSON with consistent names, either snake_case or camelCase. Pick one and keep to it.
  • Use ISO 8601 (the international date and time format) with a time zone: "2027-01-11T09:30:00Z".
  • Represent money as integers in the smallest unit (129900 paise) or as decimal strings ("1299.00"), plus a currency. Never use floats for money. A float is a number type that cannot store many decimals exactly (for example 0.1 + 0.2 is not exactly 0.3).
  • Use stable string IDs for resources. Stable means the ID never changes.
  • Use enums as strings ("status": "shipped") rather than magic numbers. An enum is a fixed list of allowed values.
  • Do not show database internals (like column names such as usr_tbl_fk) in the API. The API is a contract, not a copy of your tables.

7. Use HTTP features you get for free

An ETag is a response header that holds a version label for a resource. A client can send it back later to ask "has this changed?" or "update only if it is still this version". The diagram shows optimistic concurrency: you do not lock the data. Instead you check the version when you save, and you reject the save if someone else changed it first.

step 1 / 8
Editor A
API
Editor B
  1. GET /products/991
  2. 200 · ETag: "v7"
  3. GET /products/991
  4. 200 · ETag: "v7"
  5. PATCH · If-Match: "v7"
  6. 200 · ETag: "v8"
  7. PATCH · If-Match: "v7"
  8. 412 Precondition Failed
    re-read, merge, retry
Optimistic concurrency over HTTP with ETag and If-Match. The second editor cannot silently overwrite the first. · sequence · step through
  • Caching: add Cache-Control (how long a copy may be reused) and ETag to GET responses (lessons 3 and 14).
  • Conditional updates: clients send If-Match: "<etag>" on PUT or PATCH. If someone else changed the resource first, return 412 Precondition Failed. This is optimistic concurrency (lesson 21) over HTTP.
  • Compression: gzip or Brotli make responses smaller, so they travel faster.
  • Content negotiation: the client says which format it wants with Accept, and the server says which one it sends with Content-Type.

8. Security basics

  • Always use HTTPS (HTTP with encryption, so others cannot read or change the data on the way).
  • Put tokens in the Authorization header, never in URLs. URLs are saved in logs and browser history.
  • Check authorisation for every object. Authorisation means checking what a user is allowed to do. A user who is logged in (authenticated) may still not be allowed to read /orders/5521. This is the top API vulnerability in the OWASP API Security list. It is called IDOR (insecure direct object reference) or broken object-level authorisation. More in Part 9.
  • Validate all input (check it is the right type and size), and limit request sizes.
  • Apply rate limits (a cap on how many requests a client can make), returning 429 with Retry-After (Part 7).
  • Configure CORS (Cross-Origin Resource Sharing, the browser rule about which websites may call your API) carefully for browser clients (lesson 3).

9. Document and design the contract first

OpenAPI (formerly called Swagger) is a standard file format that describes a REST API: its URLs, inputs and outputs. You write it in YAML or JSON:

YAML
paths:  /orders/{id}:    get:      summary: Get an order      parameters:        - name: id          in: path          required: true          schema: { type: string }      responses:        "200": { description: The order }        "404": { description: Not found }

From one OpenAPI file you can generate:

  • interactive documentation,
  • client SDKs (ready-made code libraries for calling the API),
  • server stubs (empty server code with the right shape),
  • mock servers (fake servers that return sample answers),
  • contract tests (tests that check the API follows the spec).

Many teams write the spec before the code. This is called "design-first". It lets the frontend and backend teams work at the same time.

10. REST maturity: how "RESTful" do you need to be?

The Richardson Maturity Model describes levels:

  • Level 0: one endpoint, and everything is POST (like old SOAP or RPC over HTTP). RPC means calling a remote function by name.
  • Level 1: separate resources (URLs).
  • Level 2: proper HTTP methods and status codes. ← most good real-world APIs are here.
  • Level 3: HATEOAS: responses include links to the next possible actions ("cancel": {"href": "/orders/5521/cancel"}), so clients can discover what to do next.

Level 3 is elegant, but few APIs use it fully. Level 2, done consistently, is the practical goal.

Level 3 — HATEOAS
responses carry links to next actions — elegant, rarely used fully
Level 2 — HTTP verbs + status codes
GET/POST/PATCH/DELETE, 201/404/409 — ← most good APIs
Level 1 — resources
separate URLs per thing — /orders/5521
Level 0 — one endpoint
everything is POST — RPC/SOAP over HTTP
The Richardson Maturity Model. Consistent Level 2 is the practical goal. · layers

Trade-offs

  • ✅ REST is simple, universal and cacheable, and it works with every language, browser, proxy and tool.
  • ✅ It is easy to debug with curl (a command-line tool that sends HTTP requests) and browser DevTools.
  • ❌ Over-fetching and under-fetching: a fixed response may contain too much data (over-fetching), or you may need several calls to build one screen (under-fetching). GraphQL (lesson 32) tries to solve this.
  • ❌ Chatty for complex screens: many round trips (request and response pairs), which is slow on mobile networks.
  • ❌ Text-based JSON is bigger and slower to parse than binary formats, which matters for high-volume internal calls (see gRPC, lesson 31).
  • ❌ Not great for streaming or real-time updates (lesson 33).

When not to use plain REST: high-performance internal service-to-service calls (consider gRPC), clients with very varied data needs (consider GraphQL), and real-time push, where the server sends data without being asked (use WebSockets or SSE).

In the Real World

Stripe (a payments company) is often called the best example of REST API design:

  • clean resource URLs (/v1/customers, /v1/payment_intents),
  • consistent error objects with clear types and codes,
  • cursor-based pagination (a marker tells the server where the next page starts),
  • idempotency keys (a unique value per request, so a retry does not repeat the action),
  • and excellent documentation.

Many companies copy its patterns.

GitHub's REST API uses standard methods, status codes, ETag-based conditional requests (which do not count against the rate limit when nothing has changed and the server answers 304 Not Modified), pagination via Link headers, and clear rate-limit headers. It's a good reference for large public APIs.

Microsoft, Google and Zalando have all published their internal API design guidelines. Microsoft's REST API Guidelines, Google's API Improvement Proposals (AIPs) and Zalando's RESTful API Guidelines are used by thousands of engineers to keep hundreds of APIs consistent.

Public government and banking APIs. Many open-banking and public-data APIs are REST with OpenAPI specs, because REST's simplicity and wide tooling make it easy for many external developers to integrate.

Interview Questions

Q1What makes an API RESTful?

An API is RESTful when resources have URLs and you work on them with standard HTTP methods that keep their meaning (GET is safe, PUT and DELETE are idempotent). Requests are stateless, so each one carries its own auth. Responses can be cached. Status codes are meaningful, and data uses a standard format like JSON.

Q2How do you model an action like “cancel order” in REST?

Use an action sub-resource (POST /orders/5521/cancel), which is practical and common. Or create a resource (POST /order-cancellations). Never use GET, and do not hide side effects inside PATCH.

Q3Why not return 200 with an error in the body?

Monitoring, caches, load balancers, retries and client libraries all act on status codes. A 200 with an error inside looks like success to every tool in between. It hides outages and can even be cached. Return the right 4xx or 5xx code with a consistent Problem Details body.

Q4How should an API represent money?

As integers in the smallest currency unit (129900 paise) or as decimal strings, always with an explicit currency code. Never as floats, because floats cannot store many decimal values exactly.

Q5What's the most common API security bug?

Broken object-level authorisation (IDOR). The server checks that the user is logged in, but not that the user may see this specific object. So changing /orders/5521 to /orders/5522 returns someone else's order. Check ownership or permission on every object access.

Q6What is OpenAPI used for?

A contract file, readable by programs, that describes a REST API. From it you can generate docs, client SDKs, server stubs, mocks and contract tests. If you write it first, frontend and backend can work at the same time.

Key Takeaways

  • Design around resources (nouns) and standard HTTP methods (verbs). Keep nesting shallow, and use action sub-resources for non-CRUD operations.
  • Return correct status codes and consistent error bodies (Problem Details). Never return 200 for failures.
  • Support filtering, sorting, field selection and pagination with query parameters.
  • Use HTTP's built-in features, caching, ETags/If-Match and compression, and follow security basics: HTTPS, auth headers, per-object authorisation, rate limits.
  • Describe the API with OpenAPI, design the contract first, and aim for consistent Level 2 REST.

Further Reading

  • Roy Fielding's dissertation "Architectural Styles and the Design of Network-based Software Architectures" (chapter 5, "Representational State Transfer")
  • Microsoft REST API Guidelines (on GitHub)
  • Google API Improvement Proposals (AIPs)
  • Zalando RESTful API Guidelines
  • The OpenAPI Specification and RFC 9457 (Problem Details for HTTP APIs)
  • Martin Fowler's article "Richardson Maturity Model"
  • The System Design Primer on GitHub (section "Representational state transfer (REST)")

Published by Jainik · Last updated · How these lessons are made

Keep learning

Related series