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
| Method | Action | Idempotent? | Has Body? |
|---|---|---|---|
| GET | Read a resource | Yes | No |
| POST | Create a new resource | No | Yes |
| PUT | Replace an entire resource | Yes | Yes |
| PATCH | Update specific fields | Yes | Yes |
| DELETE | Remove a resource | Yes | No |
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.