REST API Design Basics

intermediate rest api design http

REST stands for Representational State Transfer. It’s an architectural style, not a protocol. There’s no official “REST spec” — it’s a set of conventions that make APIs predictable and easy to use. If we follow these conventions, any developer can look at our API and immediately know how to use it.

Resources, Not Actions

The core idea of REST is that we model everything as resources (nouns), not actions (verbs). The URL represents the resource. The HTTP method represents what we want to do with it.

# Bad — verbs in the URL
GET /getUsers
POST /createUser
POST /deleteUser/42

# Good — resources as nouns, methods as verbs
GET    /users          # list all users
POST   /users          # create a new user
GET    /users/42       # get user 42
PUT    /users/42       # replace user 42 entirely
PATCH  /users/42       # update specific fields on user 42
DELETE /users/42       # delete user 42

Nested resources show relationships naturally: /users/42/posts means “all posts belonging to user 42”. Keep nesting to at most two levels deep — anything deeper gets messy.

HTTP Methods Mapped to CRUD

MethodActionIdempotent?Has Body?
GETRead a resourceYesNo
POSTCreate a new resourceNoYes
PUTReplace an entire resourceYesYes
PATCHUpdate specific fieldsYesYes
DELETERemove a resourceYesNo

Idempotent means calling it multiple times gives the same result. Sending DELETE /users/42 twice still results in user 42 being gone. But POST /users twice creates two users — that’s why POST is not idempotent.

Path Params vs Query Params vs Request Body

Each has a clear use case. Mixing them up is a common mistake.

# Path params — identify a specific resource
GET /users/42              # user ID is part of the resource path

# Query params — filter, sort, paginate a collection
GET /users?status=active&sort=-createdAt&limit=20

# Request body — send data for create/update operations
POST /users
Content-Type: application/json
{"name": "Manish", "email": "manish@example.com"}

Rule of thumb: if it identifies which resource, put it in the path. If it modifies how we view the collection, put it in the query string. If it’s data we’re sending to the server, put it in the body.

Pagination

Never return every record in a collection. Two common patterns exist.

# Offset-based (simple but slow for large datasets)
GET /users?offset=40&limit=20
# Response includes: { "data": [...], "total": 245, "offset": 40, "limit": 20 }

# Cursor-based (better for large/real-time datasets)
GET /users?after=eyJpZCI6NDJ9&limit=20
# Response includes: { "data": [...], "next_cursor": "eyJpZCI6NjJ9" }

Offset-based is easy to implement but breaks when records are inserted or deleted between pages. Cursor-based pagination is what most production APIs use — it’s stable regardless of data changes.

Filtering and Sorting

Keep filtering conventions consistent across all endpoints.

# Filtering — use field names as query params
GET /posts?status=published&author_id=42

# Sorting — prefix with - for descending
GET /posts?sort=-createdAt        # newest first
GET /posts?sort=title             # alphabetical

# Combined
GET /posts?status=published&sort=-createdAt&limit=10

API Versioning

APIs evolve. We need a way to make breaking changes without breaking existing clients.

# URL path versioning (most common, easiest to understand)
GET /v1/users
GET /v2/users

# Header-based versioning (cleaner URLs, harder to test)
GET /users
Accept: application/vnd.myapi.v2+json

Most teams go with URL versioning because it’s visible and easy to debug. We can see the version right in the browser URL bar or in logs.

Status Codes That Matter

We don’t need to memorize all HTTP status codes. These are the ones that show up in real API design.

# Success
200 OK              # GET, PUT, PATCH succeeded
201 Created         # POST created a new resource
204 No Content      # DELETE succeeded, nothing to return

# Client errors
400 Bad Request     # invalid JSON, missing required field
401 Unauthorized    # not logged in (bad/missing auth token)
403 Forbidden       # logged in but not allowed
404 Not Found       # resource doesn't exist
409 Conflict        # duplicate email, version mismatch
422 Unprocessable   # valid JSON but failed validation

# Server errors
500 Internal Error  # something broke on our end
503 Service Unavail # server is down or overloaded

Practical Example: A Complete CRUD Flow

# Create a user
curl -X POST https://api.example.com/v1/users \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer eyJhbGc..." \
  -d '{"name": "Manish", "email": "manish@example.com"}'
# → 201 Created, returns { "id": 42, "name": "Manish", ... }

# Read that user
curl https://api.example.com/v1/users/42 \
  -H "Authorization: Bearer eyJhbGc..."
# → 200 OK

# Update their name
curl -X PATCH https://api.example.com/v1/users/42 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer eyJhbGc..." \
  -d '{"name": "Manish P"}'
# → 200 OK

# Delete
curl -X DELETE https://api.example.com/v1/users/42 \
  -H "Authorization: Bearer eyJhbGc..."
# → 204 No Content

In simple language, REST is about using URLs as nouns, HTTP methods as verbs, and status codes to tell clients what happened — keep it predictable and any developer can use our API without reading a single doc.