Skip to main content

Command Palette

Search for a command to run...

REST API Design: Stop Guessing, Start Following Patterns

Updated
13 min read

Most backend engineers write APIs. Few design them. The difference shows up every time someone integrates your work and either breezes through it — or spends half a day decoding inconsistencies.

This is the complete guide to REST API design: where it came from, what the patterns are, and how to make decisions confidently every time.


Where REST Came From

In 1990, Tim Berners-Lee built the World Wide Web to share knowledge globally. He invented URI, HTTP, HTML, the first web server, and the first web browser — all within roughly a year.

The web grew faster than he planned. Way faster.

Around 1993, Roy Fielding — co-founder of the Apache HTTP Server project — became concerned about scalability. The infrastructure wasn't built for the scale of users arriving daily. His solution: six architectural constraints that would make the web scalable by design.

Constraint What It Does
Client-Server Separates UI from business logic. They evolve independently.
Stateless Each request carries all information needed. Server holds no client context.
Caching Responses labeled cacheable or not. Reduces load, improves speed.
Uniform Interface Standardizes how components communicate.
Layered System Each layer only sees the one directly below. Enables proxies, load balancers.
Code on Demand Optional. Servers can send executable code (JavaScript) to clients.

Fielding and Berners-Lee co-authored the HTTP/1.1 spec applying these constraints. Then in 2000, Fielding published his PhD dissertation formally naming this architectural style: Representational State Transfer — REST.

Every "REST API" traces back to that document.


What REST Actually Means

The acronym breaks down into three real ideas:

Representational — resources can be expressed in multiple formats. The same user resource becomes JSON for API clients, HTML for browsers. The representation changes based on context; the resource doesn't.

State — each resource has a current state (its attributes and values). A shopping cart's state: items, quantities, price. This state is what gets transferred.

Transfer — the movement of resource representations between client and server via HTTP.

Everything else — routes, methods, status codes — is practical implementation of this idea.


URL Structure

A well-designed API URL:

https://api.example.com/v1/organizations/42/projects
Part Example Purpose
Scheme https Always HTTPS in production
Subdomain api. Separates API from the main site
Domain example.com Your service
Version /v1 Allows breaking changes without breaking existing clients
Resources /organizations/42/projects The hierarchy of what you're accessing

Resources Are Always Plural

Always. Without exception.

✅ GET /books
✅ GET /books/42
✅ POST /books

❌ GET /book
❌ GET /book/42

The resource name refers to the collection. You're always operating within that collection — whether listing all or targeting one by ID. Singular creates inconsistency. Plural is consistent regardless.

Slashes Represent Hierarchy

/organizations                       → all organizations
/organizations/42                    → organization 42
/organizations/42/projects           → all projects in that org
/organizations/42/projects/7/tasks   → all tasks in that project

Read a route top-to-bottom and the context is obvious without documentation.

URL Formatting Rules

  • All lowercase

  • Spaces → hyphens (harry-potter, not harry_potter)

  • Never underscores, never spaces

✅ /books/harry-potter-and-the-philosopher-stone
❌ /books/Harry_Potter_And_The_Philosopher_Stone

HTTP Methods — Semantics Matter

Methods exist to express intent. They answer: "What are you trying to do?"

GET    /projects      →  fetch all projects
POST   /projects      →  create a new project
PATCH  /projects/42   →  partially update project 42
PUT    /projects/42   →  completely replace project 42
DELETE /projects/42   →  delete project 42
Method Intent Has Body? Idempotent?
GET Retrieve No
POST Create / custom action Yes
PUT Full replacement Yes
PATCH Partial update Yes ❌*
DELETE Remove Optional

PATCH vs PUT — Know the Difference

// Existing record:
{
  "id": 1,
  "name": "Design System",
  "status": "active",
  "description": "Our component library",
  "owner": "priya@company.com"
}

// PATCH /projects/1
// Request: { "status": "archived" }
// Result: only status changes. Everything else stays.
{
  "id": 1,
  "name": "Design System",
  "status": "archived",
  "description": "Our component library",
  "owner": "priya@company.com"
}

// PUT /projects/1
// Request: { "status": "archived" }
// Result: everything else is GONE.
{
  "id": 1,
  "status": "archived"
}

Default to PATCH. Use PUT only when full replacement is intentionally required — which is rare.


Idempotency — The Property That Matters at Scale

Idempotent = calling the same operation multiple times has the same effect as calling it once. Not the same response — the same side effects on the server.

GET /projects/42    →  data returned, nothing changed on server
GET /projects/42    →  same ✅
GET /projects/42    →  same ✅

DELETE /projects/42  →  project deleted
DELETE /projects/42  →  404 (already gone, but nothing else changed) ✅

POST /projects  →  Project #1 created
POST /projects  →  Project #2 created ❌  (different result)
POST /projects  →  Project #3 created ❌

Why It Actually Matters

Networks fail. Requests time out. When a request fails mid-flight, should you retry?

  • Idempotent operation → retry safely. Same outcome.

  • Non-idempotent operation (POST) → retrying might create duplicates.

This is why payment systems use idempotency keys — a unique token sent with each request so the server can recognize and ignore duplicates. One missed detail here = customers charged twice.


Designing Your Resources

Before writing routes, identify your resources. A resource is any noun that represents data your system manages.

Workflow: go through your UI designs / wireframes → extract every noun that represents managed data → those are your resources.

For a project management platform (Jira/Linear-style):

Organizations  →  companies on the platform
Projects       →  work containers inside an org
Tasks          →  units of work within a project
Users          →  people using the platform
Tags           →  labels for organizing tasks
Comments       →  discussion on tasks

Each resource gets its own set of routes. All follow the same patterns.


The CRUD Pattern

Every resource will have some combination of five operations. The pattern is always the same:

Operation Method Route Status Code
Create POST /organizations 201 Created
List GET /organizations 200 OK
Get one GET /organizations/:id 200 OK
Update PATCH /organizations/:id 200 OK
Delete DELETE /organizations/:id 204 No Content

Create and List share the same route — the method differentiates them. Get, Update, and Delete share the same route structure — again, method differentiates them.

Create

POST /organizations
Content-Type: application/json

{
  "name": "Acme Corp",
  "description": "Building better anvils since 1952"
}
HTTP/1.1 201 Created

{
  "id": "org_9f2a3b",
  "name": "Acme Corp",
  "description": "Building better anvils since 1952",
  "status": "active",
  "createdAt": "2026-03-11T10:00:00Z"
}

201, not 200. id, createdAt, status are server-set — don't ask the client for them. All JSON fields in camelCase.

List

GET /organizations
HTTP/1.1 200 OK

{
  "data": [...],
  "total": 47,
  "page": 1,
  "totalPages": 5
}

Empty list? Still 200:

{ "data": [], "total": 0, "page": 1, "totalPages": 0 }

Never 404 for an empty list. 404 = a specific resource you requested doesn't exist. An empty list is a valid, successful response.

Update

PATCH /organizations/org_9f2a3b

{ "description": "Updated description" }
HTTP/1.1 200 OK

{ ...full updated resource... }

Return the complete updated object. Client sent partial data — give them back the full current state.

Delete

DELETE /organizations/org_9f2a3b
HTTP/1.1 204 No Content
(empty body)

204 — success, nothing to return. Resource is gone.


Pagination, Sorting, Filtering

Every list endpoint needs all three. Without them, your API breaks under real data volumes.

Pagination

GET /organizations?page=2&limit=10
{
  "data": [...10 orgs...],
  "total": 47,
  "page": 2,
  "totalPages": 5
}

Without pagination, a 50,000-record database table means serializing 50,000 records, transmitting them, and forcing the client to parse all of it — for a user who might see 20 on screen.

Always set sane defaults server-side. If no page or limit is sent, default to page=1, limit=10. Never force clients to send obvious parameters.

Sorting

GET /organizations?sortBy=name&sortOrder=asc

Two parameters: sortBy (which field) and sortOrder (asc or desc).

Always have a default sort. Without one, result order varies between calls as databases return records in arbitrary sequence. Default: sortBy=createdAt&sortOrder=desc (latest first) is almost always the right call.

Filtering

GET /organizations?status=active
GET /organizations?status=active&name=acme

Filter params map to resource fields. Multiple filters combine. The client asks for exactly what it needs.

All Three Together

GET /organizations?page=1&limit=10&sortBy=createdAt&sortOrder=desc&status=active

This is the full pattern. Every list endpoint should support it.


Custom Actions — When CRUD Isn't Enough

Some operations don't fit create, read, update, or delete.

Example: archiving an organization.

Sounds like PATCH /organizations/42 with { "status": "archived" }. But archiving might also need to:

  • Deactivate all projects under that org

  • Send notification emails to all members

  • Archive all tasks in those projects

  • Log an audit event

That's not an update. It's an action. A simple status patch doesn't carry that semantic weight.

Use POST with the action name:

POST /organizations/42/archive
POST /projects/7/clone
POST /tasks/3/assign
POST /invoices/12/send

Structure:

/organizations        →  the collection
/organizations/42     →  a specific resource
/organizations/42/archive  →  an action on that resource

Response for archive (no new resource created):

HTTP/1.1 200 OK
{ ...updated organization... }

Response for clone (new resource created):

HTTP/1.1 201 Created
{ ...newly cloned project... }

The response code follows what actually happened on the server, not the method. Don't assume POST always returns 201.


Response Design

Success Responses

Single resource:

{
  "id": "org_9f2a3b",
  "name": "Acme Corp",
  "status": "active",
  "createdAt": "2026-03-11T10:00:00Z"
}

Collection:

{
  "data": [...],
  "total": 47,
  "page": 1,
  "totalPages": 5
}

Error Responses

Standardize these early. Inconsistent error shapes are one of the biggest frustrations when integrating an API.

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request body contains invalid fields",
    "details": [
      { "field": "email", "message": "Must be a valid email address" },
      { "field": "name", "message": "Required field missing" }
    ]
  }
}

code = machine-readable. Clients branch logic on this. message = human-readable. For developers debugging. details = optional. Field-level errors for validation failures.

Status Code Cheat Sheet

Scenario Code
Successful GET or PATCH 200 OK
Successful POST (new resource) 201 Created
Successful DELETE 204 No Content
Custom action, no new resource 200 OK
Invalid request / missing fields 400 Bad Request
Missing or expired token 401 Unauthorized
Authenticated, not authorized 403 Forbidden
Specific resource not found 404 Not Found
Empty list result 200 OK (not 404)
Duplicate / state conflict 409 Conflict
Valid format, invalid data 422 Unprocessable Entity
Rate limit hit 429 Too Many Requests
Server crash 500 Internal Server Error

The Consistency Rules

This is the thing that separates good APIs from painful ones.

Plural everywhere. Not sometimes. Not mostly. Always.

camelCase for all JSON. Not snake_case. Not PascalCase. camelCase. Every payload, every response.

✅ { "createdAt": "...", "organizationId": "...", "totalPages": 5 }
❌ { "created_at": "...", "organization_id": "...", "total_pages": 5 }

No abbreviations. description not desc. organization not org. Whoever integrates your API doesn't have your mental context.

Sane defaults on everything. Don't force clients to send obvious parameters. If page isn't sent, default to 1. If sortOrder isn't sent, default to desc.

Consistent error shapes. If one endpoint returns { "error": { "message": "..." } }, they all do.

Consistent field names across resources. If one resource uses description, the next one doesn't get to use summary or desc. When a frontend engineer integrates their first endpoint, they make assumptions about your other endpoints. Make those assumptions correct.


The Design Workflow

Here's the right order of operations before writing any implementation code:

1. Understand the UI — look at wireframes / Figma designs. This is where your requirements live.

2. Identify resources — extract nouns from requirements. Those are your resources.

3. Design the database schema — map resources to tables. Think relationships and foreign keys.

4. Design the API interface in a client tool — open Postman or Insomnia. Create every request. Define routes, methods, payloads, expected responses. This is design, not implementation.

5. Write the code — now implement. Every interface decision is already made.

This sounds slower. It's faster. Design mistakes caught at step 4 take minutes to fix. Design mistakes caught at step 5+ take hours.


Interview Questions

"What is REST?" An architectural style for networked APIs defined by Roy Fielding in his 2000 PhD dissertation. Not a protocol. Six constraints: client-server, stateless, cacheable, uniform interface, layered system, code on demand (optional).

"Why are resource names plural?" They refer to collections. Plural works consistently — whether listing all items or targeting one by ID. Singular creates inconsistency.

"PATCH vs PUT?" PUT replaces the entire resource. Fields you don't send get deleted. PATCH does a partial update — only sent fields change. Default to PATCH.

"What is idempotency?" Calling an operation N times has the same server-side effect as calling it once. Matters for retry logic. GET, PUT, DELETE are idempotent. POST is not.

"When do you use POST for non-create operations?" For custom actions that don't fit CRUD — POST /organizations/42/archive, POST /projects/7/clone. POST is open-ended in the REST spec for exactly this reason.

"404 vs empty array on a list endpoint?" Empty list → 200 OK with "data": []. 404 is for when a specific requested resource doesn't exist. An empty list is a valid successful response.

"What's in a paginated response?" data (current page's records), total (count across all pages), page (current page number), totalPages (total pages count).

"Status code for DELETE?" 204 No Content. Resource is gone, nothing meaningful to return.


This is from series Backend Engineering from First Principles. Next: Mastering Databases.

Backend First Principles

Part 12 of 17

This series documents my learning journey through the "Backend from First Principles" playlist. Instead of jumping directly into frameworks, the focus is on understanding the core concepts that power backend systems. Throughout this series, I explore how backend systems actually work — from the request-response lifecycle, HTTP fundamentals, routing, serialization, authentication, and validation to more advanced topics like caching, task queues, observability, security, and scaling. The goal of this series is to build a strong conceptual foundation for backend engineering that applies across languages and frameworks. By learning backend development from first principles, we gain a deeper understanding of how modern web systems are designed, built, and scaled.

Up next

API Validations & Transformations: The Gatekeepers of Your Backend

Every piece of data your server blindly trusts is a vulnerability waiting to happen. Here's the system that keeps your backend honest. Why This Matters More Than You Think Imagine a hospital that le