Home
Backend from First Principles / Module 11 — RESTful API Design

RESTful API Design

Resources are nouns. Pagination strategies. Response shape standards. HATEOAS.


Where REST Came From

Before there was REST, there was the World Wide Web — and that's not a coincidence.

In 1989, Tim Berners-Lee at CERN built the Web on three technologies:
• URL — a unique address for any resource
• HTTP — a way to retrieve resources by URL
• HTML — a format for those resources

It was originally meant for sharing physics papers within CERN. When CERN released it publicly in 1993, the scale exploded and the cracks showed. Every hyperlink click opened a brand new TCP connection. State was a mess. Caching was haphazard.

In 2000, Roy Fielding (one of the authors of the HTTP/1.1 specification) wrote his PhD thesis on the architectural constraints that made the Web work — and called the style "REST" (Representational State Transfer). It wasn't a new protocol. It was a description of what the Web was already doing well, formalized so other systems could be built the same way.

What was REST competing against?

SOAP (Simple Object Access Protocol) — popular in the early 2000s. SOAP wraps every remote call in XML, sends it over HTTP, and defines strict contracts using yet another XML format (WSDL). It was rigid, verbose, and notably: SOAP only used POST. It treated HTTP as a dumb tunnel, ignoring caching, status codes, and method semantics.

REST won because it embraced HTTP rather than tunneling through it. Caching, status codes, methods — REST uses them all. APIs became simpler, more cacheable, and language-agnostic in a way SOAP never managed.

That's the inheritance: every REST API you build today is a direct descendant of how Tim Berners-Lee designed the Web. The constraints aren't arbitrary — they're what made the Web scale to billions of users.


REST Principles

REST is an architectural style, not a protocol. Six constraints define it:

  1. Client-Server — Separation of concerns. UI from data storage.
  2. Stateless — Each request contains all info needed. No session state on server.
  3. Cacheable — Responses declare if they're cacheable.
  4. Uniform Interface — Resources identified by URIs, manipulated via representations.
  5. Layered System — Client can't tell if it's talking to the real server or a proxy.
  6. Code on Demand (optional) — Server can send executable code (JS) to clients.

Most "REST APIs" only follow 1-4. That's fine. True REST (HATEOAS) is rarely implemented.

Three powerful ideas hide inside the Uniform Interface constraint:
• Everything addressable has a URI. /users/42 isn't just data — it's the unique address of that resource.
• Never transfer the resource itself, only a representation. The same /users/42 resource could be returned as JSON, XML, or HTML depending on the Accept header.
• Responses must be self-describing. The server includes enough metadata (Content-Type, status, headers) for the client to know how to process the response, without needing out-of-band documentation.


Idempotency — The Quietly Important Property

An operation is idempotent if calling it multiple times has the same effect as calling it once.

GET /users/42 — idempotent. Reading doesn't change anything.
PUT /users/42 — idempotent. Replacing with the same data leaves the same result.
DELETE /users/42 — idempotent. After the first call, subsequent calls do nothing new.
POST /users — NOT idempotent. Each call creates a new user.

Why this matters in practice:

Network failures. If a request times out, can the client safely retry? For idempotent methods, yes. For POST, no — without extra machinery, you might create duplicates. (See: idempotency keys, used by Stripe, AWS, and others.)

Caching. Only safe (idempotent and side-effect-free) responses can be cached. That's why GET responses are routinely cached and POST responses aren't.

Designing for failure. When you're choosing between PUT and POST, "is this safe to retry?" is one of the best lenses to use. Pick the idempotent verb when you can; it's a gift to every client integrating with your API.


Resource Design

Resources are nouns, not verbs. URLs identify things, HTTP methods describe the action.

Snippet
Bad:
  GET  /getUsers
  POST /createUser
  POST /deleteUser/123
Snippet
Good:
  GET    /users           → list users
  POST   /users           → create user
  GET    /users/123       → get user 123
  PUT    /users/123       → replace user 123
  PATCH  /users/123       → partially update user 123
  DELETE /users/123       → delete user 123
Snippet
Nested resources for relationships:
  GET /users/123/posts        → posts by user 123
  POST /users/123/posts       → create post for user 123
  GET /users/123/posts/456    → specific post by user 123

Don't nest deeper than 2 levels. /a/1/b/2/c/3/d/4 is unnavigable.


Pagination

Never return unbounded lists. Always paginate.

Snippet
Offset pagination (simple, standard):
  GET /users?page=2&limit=20
  Response: { data: [...], total: 500, page: 2, totalPages: 25 }
  Problem: Skipping offset=10000 still scans 10000 rows. Slow on large tables.
Snippet
Cursor pagination (for feeds, high performance):
  GET /users?cursor=eyJ1c2VySWQiOiI1MDB9&limit=20
  Response: { data: [...], nextCursor: "eyJ1c2VySWQiOiI1MjB9" }
  Benefits: O(1) regardless of position, stable results during inserts.
Snippet
Keyset pagination (similar to cursor, database-native):
  GET /users?after_id=500&limit=20
  SQL: WHERE id > 500 ORDER BY id LIMIT 20

Use offset for admin UIs. Use cursor/keyset for public feeds and infinite scroll.


Response Shape Standards

Consistency matters more than perfection. Pick a shape and never deviate.

Snippet
Success (single resource):
  { "data": { "id": "123", "name": "Alice", "email": "alice@example.com" } }
Snippet
Success (list):
  {
    "data": [...],
    "meta": { "total": 100, "page": 1, "limit": 20 }
  }
Snippet
Error:
  {
    "error": {
      "code": "VALIDATION_ERROR",
      "message": "Request validation failed",
      "details": [{ "field": "email", "message": "Invalid email" }]
    }
  }

Timestamp fields: always ISO 8601 UTC. "2024-01-15T10:30:00Z"
IDs: string (not integer) — future-proof and avoids JS precision issues.


HATEOAS

HATEOAS (Hypermedia as the Engine of Application State) is the most misunderstood REST constraint. A truly RESTful API returns links to related actions in every response:

JavaScript
{
  "data": {
    "id": "123",
    "status": "pending",
    "_links": {
      "self":    { "href": "/orders/123" },
      "cancel":  { "href": "/orders/123/cancel", "method": "POST" },
      "payment": { "href": "/orders/123/payment" }
    }
  }
}

The client discovers what it can do next from the response itself — no out-of-band documentation needed. GitHub's API partially implements this.

Rarely worth full implementation. Understand the concept; apply where it adds value.


Source & Credit

The Backend from First Principles series is based on what I learnt from Sriniously's YouTube playlist — a thoughtful, framework-agnostic walk through backend engineering. If this material helped you, please go check the original out: youtube.com/@Sriniously. The notes here are my own restatement for revisiting later.

⁂ Back to all modules