All articles
Backend from First Principles

REST API Design: A Mental Model for Backend Engineers

You'll spend more time designing APIs than almost any other backend task. A well-designed API is invisible — developers integrate and move on. A poorly designed one generates Slack messages at 2 AM asking "what does this endpoint even return?"

2026-03-1310 min read

This is the seventh fundamental I'm revisiting in my backend-from-first-principles series. All the previous topics — HTTP, routing, serialization, auth, validation, architecture — converge here. REST API design is where you put it all together into something other developers (and your future self) actually use.


What REST Actually Means

REST stands for Representational State Transfer. Let's break those three words apart because they hold the entire philosophy:

  • Representational — A resource (like a user or a product) can have multiple representations. An API client gets JSON. A browser gets HTML. The underlying data is the same; the format changes based on who's asking.
  • State — The current condition of a resource. A shopping cart has items, quantities, and a total price at any given moment. That's its state.
  • Transfer — Moving those representations between client and server using standard HTTP methods.

REST was proposed by Roy Fielding in 2000 to solve the web's scalability crisis. It's not a library or a framework — it's a set of architectural constraints. Follow them, and your system scales. Violate them, and you're swimming upstream.

The Six Constraints

A truly RESTful system follows these rules:

  1. Client-Server — Strict separation. Client handles UI, server handles data. (We covered this in the HTTP article.)
  2. Stateless — Server retains no memory between requests. Every request is self-contained. (Also covered in HTTP.)
  3. Cacheable — Responses must explicitly declare if they're cacheable or not. (The caching conversation from HTTP.)
  4. Uniform Interface — Everyone communicates the same way. Same URL patterns, same methods, same conventions.
  5. Layered System — You can insert proxies, load balancers, and gateways between client and server. Each layer only sees the layer directly below it.
  6. Code on Demand (optional) — Servers can send executable code (like JavaScript) to extend client functionality.

Notice how constraints 1-3 are things we've already internalized from previous topics. REST is the architectural style that ties everything together.


The Mental Model: Designing a Library System

Imagine you're designing the card catalog for a public library. Every book, every member, every loan needs to be findable through a consistent, predictable system.

You wouldn't label one shelf "getBooks" and another "fetch_all_members" and another "LOANS-list." You'd create a uniform system where anyone can walk in and find anything without asking for help.

That's REST API design — building a uniform, predictable, self-documenting interface that any developer can navigate without reading your source code.


Anatomy of a RESTful URL

Every REST API URL follows a consistent structure:

https://api.example.com/v1/organizations/123/projects

  https         → Scheme (always HTTPS)
  api.          → API subdomain
  example.com   → Domain
  v1            → API version
  organizations → Resource (plural noun)
  123           → Specific resource ID
  projects      → Nested resource

The Naming Rules

These rules aren't suggestions — they're the difference between an API that feels intuitive and one that drives developers insane:

  • Always use plural nouns. /users, /projects, /organizations. Even when fetching a single item: /users/123 — not /user/123. The collection stays plural; the ID narrows it down.
  • Never use verbs in URLs. The HTTP method IS the verb.
    • Wrong: GET /getUsers or POST /createUser
    • Right: GET /users or POST /users
  • Use hyphens for readability. /project-members — not /project_members or /projectMembers. URLs are lowercase with hyphens.
  • Use camelCase in JSON payloads. { "firstName": "Ali", "createdAt": "..." }. URLs use hyphens, JSON uses camelCase. Be consistent.
  • Use / to express hierarchy. /organizations/123/projects reads as "projects belonging to organization 123." Each slash zooms one level deeper.

Methods + Resources: The Complete CRUD Map

The HTTP method declares the action. The URL declares the resource. Together, they form a sentence:

GET    /projects         → "List all projects"
GET    /projects/123     → "Get project 123"
POST   /projects         → "Create a new project"
PATCH  /projects/123     → "Partially update project 123"
PUT    /projects/123     → "Fully replace project 123"
DELETE /projects/123     → "Delete project 123"

Six endpoints. No ambiguity. Any developer can look at these and immediately understand what they do. That's the power of a uniform interface.

PATCH vs PUT: The Distinction That Matters

PATCH — Partial update. Send only the fields you want to change:

PATCH /projects/123
{ "status": "archived" }
→ Only the status field changes. Everything else stays the same.

PUT — Full replacement. Send the entire resource. Anything you don't include gets wiped:

PUT /projects/123
{ "name": "New Name", "status": "active", "description": "..." }
→ The entire resource is replaced with this payload.

In practice, PATCH is used far more often because most updates are partial — changing a status, updating a name, toggling a setting.

Idempotency Recap

This was covered in the HTTP article, but it's essential context for API design:

  • GET, PUT, PATCH, DELETE — Idempotent. Running them multiple times produces the same server state.
  • POST — NOT idempotent. Each call creates a new resource. Send the same POST 10 times, get 10 entries in your database.

This is why POST is used for creation and all other actions that need to happen exactly once unless protected by idempotency keys.


Custom Actions: When CRUD Isn't Enough

Sometimes an action doesn't fit neatly into Create, Read, Update, or Delete. Archiving an organization, cloning a project, or sending an email — these trigger complex workflows beyond a simple database write.

The REST convention:

  • Use POST as the method
  • Append the action as a verb at the end of the resource URL
POST /projects/123/clone        → Clone project 123
POST /organizations/5/archive   → Archive organization 5
POST /users/42/send-invite      → Send invite to user 42

This is the one exception where a verb appears in the URL. It's acceptable because the action genuinely doesn't map to CRUD. Don't abuse it — if an action can be expressed as a PATCH (like changing a status field), use PATCH instead.


Designing List APIs: Pagination, Sorting, and Filtering

This is where API design gets practical. A GET /users endpoint that returns 50,000 records will crash mobile clients, bottleneck networks, and make serialization/deserialization painfully slow.

Pagination — Serving Data in Chunks

Instead of returning everything, return one page at a time:

GET /organizations?page=2&limit=20

The response includes metadata so the client knows where it stands:

{
  "data": [ ... 20 organizations ... ],
  "total": 150,
  "page": 2,
  "totalPages": 8
}

A production note: This offset-based pagination (page=2&limit=20) works well for small-to-medium datasets. But at scale (100K+ records), it degrades — the database still scans through all skipped rows. For large datasets and real-time feeds, cursor-based pagination maintains constant performance by using an opaque pointer to the last seen record instead of a page number. You'll encounter both patterns.

Sorting — Let Clients Control Order

GET /organizations?sortBy=name&sortOrder=asc

The client specifies which field to sort by and the direction. Without explicit sorting, databases don't guarantee order. Consecutive requests to the same endpoint might return rows in different sequences.

Filtering — Narrow Down Results

GET /organizations?status=active&name=acme

Filter by any relevant field. This is how list APIs handle search and discovery — query parameters act as filters on the collection.

Combining Everything

A real-world list API call often combines all three:

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

Page 1, 10 results, sorted newest first, only active organizations. Clean, readable, predictable.


Sane Defaults: The Defensive Design Pattern

Your server should never crash or behave unpredictably because a client forgot an optional parameter. This is where "sane defaults" come in:

  • No page sent? → Default to 1
  • No limit sent? → Default to 10 (and cap at a max like 100 to prevent abuse)
  • No sortBy sent? → Default to createdAt
  • No sortOrder sent? → Default to desc (newest first)
  • Creating an organization without a status? → Default to "active"

Sane defaults mean the API works correctly with zero query parameters. Every parameter becomes an optional refinement, not a requirement.


Status Codes: The Ones That Matter for REST

We covered all status codes in the HTTP article. For REST API design specifically, these are the rules:

  • 200 OK — Successful GET, PATCH, PUT, or custom action
  • 201 Created — Successful POST that created a new resource. Always 201, never 200 for creation.
  • 204 No Content — Successful DELETE. The resource is gone; there's nothing to return.
  • 400 Bad Request — Client sent invalid data (validation failure)
  • 404 Not Found — Specific resource ID doesn't exist

The 404 vs 200 Rule

This trips up a lot of engineers:

  • GET /users/999 → User 999 doesn't exist → 404 Not Found. The specific resource can't be located.
  • GET /users?name=Zack → No users named Zack → 200 OK with { "data": [] }. The collection exists; the filter returned empty results.

A 404 means "this thing doesn't exist." An empty list means "this thing exists, but nothing matches your criteria right now." They're semantically different.


The Design-First Workflow

One of the most practical insights from this topic: don't start by writing code. Start from the UI.

  1. Look at the wireframes/Figma. What data does the user see? What can they interact with?
  2. Extract the nouns. Users, Projects, Organizations, Tasks, Tags — these become your resources.
  3. Design the database schema. How do these resources relate to each other?
  4. Define CRUD actions. Which resources need create, read, update, delete?
  5. Design the API interface. Map resources to URLs, methods to actions, query params to filters.
  6. Document with Swagger/OpenAPI. Generate an interactive playground before writing a single line of backend code.
  7. Then write code.

This workflow prevents a common trap: building an API that serves the database structure instead of the user experience. The frontend defines what data is needed; the backend figures out how to serve it.


Response Structure: Consistency Is Everything

Every response from your API should follow the same shape. Always.

Success:

{
  "data": { ... },
  "message": "success"
}

List success:

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

Error:

{
  "error": {
    "message": "Organization name is required",
    "code": "VALIDATION_ERROR"
  }
}

A frontend developer should never have to guess whether the response wraps data in data, result, payload, or response. Pick one structure and use it everywhere. Inconsistency is the number one source of integration bugs.


The Golden Rules Checklist

Pin this somewhere visible when designing APIs:

  • Plural nouns in URLs/users, not /user
  • No verbs in URLs — HTTP methods are the verbs (except custom actions)
  • camelCase in JSONcreatedAt, not created_at
  • Hyphens in URLs/project-members, not /project_members
  • Always version your API/v1/, /v2/
  • Sane defaults for everything — no parameter should crash the server
  • Consistent response structure — same shape, every endpoint, no exceptions
  • 201 for creation, 204 for deletion — not 200 for everything
  • Empty array, not 404, for empty list results
  • Pagination on every list endpoint — no exceptions, even if you "only have 10 items" today
  • Document with Swagger/OpenAPI — if it isn't documented, it doesn't exist

The Mental Model Summary

Carry this forward:

  • REST = An architectural style, not a technology. Constraints that make systems scalable.
  • URL = A sentence. The method is the verb, the resource is the noun. GET /users/123 reads as "get user 123."
  • CRUD = Four operations (Create, Read, Update, Delete) mapped to four methods (POST, GET, PATCH/PUT, DELETE). Custom actions use POST with a verb appended.
  • List APIs = Always paginate, always sort, always filter. Never return unbounded data.
  • Sane defaults = The API works with zero parameters. Every parameter is a refinement.
  • Consistency = Same naming, same response shapes, same conventions. A developer should predict your API's behavior without reading docs.
  • Design-first = Start from the UI, extract nouns, design the interface, document it, then code.

A great API is like a great library catalog — anyone can walk in, understand the system in minutes, and find exactly what they need. That's the benchmark. If a developer integrating your API needs to ask you questions, the design has failed.


This is part 7 of my "Backend from First Principles" series. Everything in this article comes from what I learned watching the Backend from First Principles playlist by Sriniously — specifically the video "Complete REST API Design." I'm not claiming original research. This is me taking what I studied, internalizing it, and presenting it as a mental model that made the concepts stick for me. If it helps you too, even better.

Follow along for the next one.

#BackendEngineering #RESTAPI #APIDesign #WebDevelopment #SoftwareEngineering #SystemDesign #LearningInPublic