How to design clean, predictable APIs that any developer can understand in 5 seconds.
Before I understood REST, my API routes looked like this:
GET /getUsers
POST /createNewUser
GET /fetchSingleUser?id=5
POST /deleteUser
GET /updateUserName
They worked. But they were inconsistent, confusing, and every route had its own naming convention. If another developer looked at my API, they'd have to read every single route to figure out what was going on.
Then I learned REST — a set of principles that tell you exactly how to name routes, which HTTP methods to use, and how to structure your API so it's predictable, clean, and standard. The same routes above became:
GET /users
POST /users
GET /users/5
DELETE /users/5
PUT /users/5
No verbs in URLs. Consistent patterns. Any developer who knows REST can look at these routes and immediately understand every one of them — without reading a single line of documentation.
That transformation happened for me in the ChaiCode Web Dev Cohort 2026. Let me show you how REST works.
What Does REST API Mean?
Let's break the term down:
API — Application Programming Interface. It's a way for two programs to communicate. Your frontend (React, a mobile app, or even another server) sends a request to the backend, and the backend sends a response.
┌──────────┐ HTTP Request ┌──────────┐
│ │ ──────────────────→ │ │
│ Client │ │ Server │
│ (React, │ ←────────────────── │ (Express │
│ mobile) │ HTTP Response │ API) │
└──────────┘ └──────────┘
REST — Representational State Transfer. It's a set of design principles for how APIs should be structured. It's not a technology or framework — it's a style guide.
A REST API is an API that follows REST principles: using standard HTTP methods, organizing data around resources, and communicating with standard status codes.
Resources in REST Architecture
The core idea of REST is simple: everything is a resource.
A resource is any entity your application manages — users, products, orders, comments, posts. Each resource has a URL (endpoint) that represents it.
Resources in a typical app:
/users → The collection of all users
/users/42 → A specific user (ID: 42)
/products → The collection of all products
/products/7 → A specific product (ID: 7)
/orders → The collection of all orders
/orders/101 → A specific order (ID: 101)
REST Naming Conventions
✅ DO:
/users ← plural nouns
/products ← describes WHAT, not what to DO
/orders/101 ← specific resource by ID
❌ DON'T:
/getUsers ← verb in URL (the HTTP method IS the verb)
/createProduct ← action in URL
/deleteOrder ← action in URL
/user ← singular (use plural for collections)
The golden rule: URLs are nouns, HTTP methods are verbs. The URL tells you what resource. The method tells you what action to perform on it.
HTTP Methods — The Verbs of REST
REST uses standard HTTP methods to perform actions on resources. Each method maps to a CRUD (Create, Read, Update, Delete) operation:
CRUD vs HTTP Methods Mapping
| CRUD Operation | HTTP Method | Express | URL | What It Does |
|---|---|---|---|---|
| Create | POST |
app.post() |
/users |
Create a new user |
| Read | GET |
app.get() |
/users |
Get all users |
| Read | GET |
app.get() |
/users/42 |
Get a specific user |
| Update | PUT |
app.put() |
/users/42 |
Replace/update a user completely |
| Delete | DELETE |
app.delete() |
/users/42 |
Delete a specific user |
The Pattern
Resource: users
GET /users → Get ALL users (Read — collection)
GET /users/:id → Get ONE user (Read — single)
POST /users → Create a NEW user (Create)
PUT /users/:id → Update an EXISTING user (Update)
DELETE /users/:id → Delete a user (Delete)
Same URL pattern, different HTTP methods = different actions.
GET — Reading Data
GET requests retrieve data. They should never modify anything on the server.
// Get all users
app.get("/api/users", (req, res) => {
res.json(users);
});
// Get a single user
app.get("/api/users/:id", (req, res) => {
const user = users.find((u) => u.id === parseInt(req.params.id));
if (!user) return res.status(404).json({ error: "User not found" });
res.json(user);
});
POST — Creating Data
POST requests create new resources. The data comes in the request body.
app.post("/api/users", (req, res) => {
const { name, email, role } = req.body;
if (!name || !email) {
return res.status(400).json({ error: "Name and email are required" });
}
const newUser = {
id: users.length + 1,
name,
email,
role: role || "member",
};
users.push(newUser);
res.status(201).json(newUser);
});
PUT — Updating Data
PUT requests replace an existing resource with new data.
app.put("/api/users/:id", (req, res) => {
const user = users.find((u) => u.id === parseInt(req.params.id));
if (!user) return res.status(404).json({ error: "User not found" });
const { name, email, role } = req.body;
user.name = name || user.name;
user.email = email || user.email;
user.role = role || user.role;
res.json(user);
});
DELETE — Removing Data
DELETE requests remove a resource.
app.delete("/api/users/:id", (req, res) => {
const index = users.findIndex((u) => u.id === parseInt(req.params.id));
if (index === -1) return res.status(404).json({ error: "User not found" });
users.splice(index, 1);
res.status(204).send(); // 204 = No Content (successfully deleted, nothing to return)
});
Status Codes Basics
HTTP status codes tell the client what happened. They're numbers grouped by category:
1xx — Informational (rarely used directly)
2xx — Success ✅
3xx — Redirection ↗️
4xx — Client Error ❌ (the client did something wrong)
5xx — Server Error 💥 (the server did something wrong)
Status Codes You'll Use Daily
| Code | Name | When to Use | Express Example |
|---|---|---|---|
| 200 | OK | Successful GET, PUT | res.status(200).json(data) |
| 201 | Created | Successful POST — new resource created | res.status(201).json(user) |
| 204 | No Content | Successful DELETE — nothing to return | res.status(204).send() |
| 400 | Bad Request | Client sent invalid data | res.status(400).json(err) |
| 401 | Unauthorized | No authentication provided | res.status(401).json(err) |
| 403 | Forbidden | Authenticated but not authorized | res.status(403).json(err) |
| 404 | Not Found | Resource doesn't exist | res.status(404).json(err) |
| 500 | Internal Server Error | Something broke on the server | res.status(500).json(err) |
Matching Status Codes to Operations
GET /users → 200 (here's your data)
GET /users/999 → 404 (that user doesn't exist)
POST /users → 201 (user created!)
POST /users → 400 (invalid data — name missing)
PUT /users/42 → 200 (user updated)
DELETE /users/42 → 204 (deleted, nothing to show)
DELETE /users/999 → 404 (can't delete what doesn't exist)
REST Request-Response Lifecycle
Here's the complete picture of how a REST API request flows through Express:
Client: POST /api/users { "name": "Pratham", "email": "p@dev.in" }
│
↓
┌──────────────────────────────────────────────────────┐
│ EXPRESS SERVER │
│ │
│ 1. Middleware: │
│ express.json() → parses body into req.body │
│ logger → logs "POST /api/users" │
│ │
│ 2. Router matches: │
│ app.post("/api/users") ← MATCH ✅ │
│ │
│ 3. Route handler runs: │
│ → Validates req.body (name and email present?) │
│ → Creates new user object │
│ → Adds to database / array │
│ → Sends response │
│ │
│ 4. Response: │
│ Status: 201 Created │
│ Body: { "id": 4, "name": "Pratham", ... } │
│ │
└──────────────────────────────────────────────────────┘
│
↓
Client receives: 201 { "id": 4, "name": "Pratham", "email": "p@dev.in" }
Complete Example: Users REST API
Let's build a complete, RESTful Users API from scratch:
const express = require("express");
const app = express();
app.use(express.json());
// In-memory "database"
let users = [
{ id: 1, name: "Pratham", email: "pratham@dev.in", role: "developer" },
{ id: 2, name: "Arjun", email: "arjun@dev.in", role: "designer" },
{ id: 3, name: "Priya", email: "priya@dev.in", role: "manager" },
];
let nextId = 4;
// ── GET /api/users — Get all users ──
app.get("/api/users", (req, res) => {
// Optional filtering with query params
let result = [...users];
if (req.query.role) {
result = result.filter((u) => u.role === req.query.role);
}
res.json({
count: result.length,
users: result,
});
});
// ── GET /api/users/:id — Get single user ──
app.get("/api/users/:id", (req, res) => {
const user = users.find((u) => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: "User not found" });
}
res.json(user);
});
// ── POST /api/users — Create new user ──
app.post("/api/users", (req, res) => {
const { name, email, role } = req.body;
// Validation
if (!name || !email) {
return res.status(400).json({
error: "Validation failed",
details: {
name: !name ? "Name is required" : undefined,
email: !email ? "Email is required" : undefined,
},
});
}
// Check for duplicate email
if (users.find((u) => u.email === email)) {
return res.status(400).json({ error: "Email already exists" });
}
const newUser = {
id: nextId++,
name,
email,
role: role || "member",
};
users.push(newUser);
res.status(201).json(newUser);
});
// ── PUT /api/users/:id — Update user ──
app.put("/api/users/:id", (req, res) => {
const user = users.find((u) => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: "User not found" });
}
const { name, email, role } = req.body;
user.name = name || user.name;
user.email = email || user.email;
user.role = role || user.role;
res.json(user);
});
// ── DELETE /api/users/:id — Delete user ──
app.delete("/api/users/:id", (req, res) => {
const index = users.findIndex((u) => u.id === parseInt(req.params.id));
if (index === -1) {
return res.status(404).json({ error: "User not found" });
}
const deleted = users.splice(index, 1)[0];
res.json({ message: "User deleted", user: deleted });
});
// ── 404 handler — route not found ──
app.use((req, res) => {
res.status(404).json({ error: `Route ${req.method} ${req.url} not found` });
});
const PORT = 3000;
app.listen(PORT, () => {
console.log(`REST API running at http://localhost:${PORT}/api/users`);
});
Testing the API
# Get all users
curl http://localhost:3000/api/users
# Get all developers
curl http://localhost:3000/api/users?role=developer
# Get user by ID
curl http://localhost:3000/api/users/1
# Create a new user
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{"name": "Neha", "email": "neha@dev.in", "role": "tester"}'
# Update a user
curl -X PUT http://localhost:3000/api/users/1 \
-H "Content-Type: application/json" \
-d '{"name": "Pratham Bhardwaj", "role": "senior developer"}'
# Delete a user
curl -X DELETE http://localhost:3000/api/users/2
Route Summary
| Method | Endpoint | Body | Response | Status |
|---|---|---|---|---|
| GET | /api/users |
— | All users | 200 |
| GET | /api/users/1 |
— | Single user | 200 |
| GET | /api/users/999 |
— | Error | 404 |
| POST | /api/users |
{ name, email, role } |
Created user | 201 |
| POST | /api/users |
{ } (missing fields) |
Validation error | 400 |
| PUT | /api/users/1 |
{ name, role } |
Updated user | 200 |
| DELETE | /api/users/2 |
— | Deleted user | 200 |
REST Design Principles — Quick Reference
| Principle | What It Means |
|---|---|
| Use nouns in URLs |
/users, not /getUsers or /createUser
|
| Use plural nouns |
/users, not /user
|
| HTTP methods = actions | GET reads, POST creates, PUT updates, DELETE removes |
| Use proper status codes | 201 for creation, 404 for not found, etc. |
| Nest related resources |
/users/42/orders — orders belonging to user 42 |
| Use query for filtering |
/users?role=admin&sort=name, not /users/role/admin
|
| Be consistent | Same patterns across all resources |
| Stateless | Each request contains all info needed — no server memory |
Let's Practice: Hands-On Assignment
Part 1: Design REST Routes
Before writing any code, design the routes for a blog API with two resources: posts and comments.
Posts:
GET /api/posts → All posts
GET /api/posts/:id → Single post
POST /api/posts → Create post
PUT /api/posts/:id → Update post
DELETE /api/posts/:id → Delete post
Comments (nested under posts):
GET /api/posts/:id/comments → All comments on a post
POST /api/posts/:id/comments → Add comment to a post
DELETE /api/posts/:postId/comments/:commentId → Delete a comment
Part 2: Build the Posts API
Implement the 5 post routes with validation (title and content required for POST).
Part 3: Add Nested Comments
Add the comment routes. Each comment should reference its parent post's ID.
Key Takeaways
- REST is a design style, not a technology. It uses standard HTTP methods as verbs and resource URLs as nouns to create predictable, consistent APIs.
- CRUD maps to HTTP methods: Create → POST, Read → GET, Update → PUT, Delete → DELETE. Each action has a specific method — don't use GET for everything.
-
URLs should be nouns (
/users,/products), never verbs (/getUsers). The HTTP method already tells you the action. - Status codes communicate outcomes: 200 for success, 201 for created, 400 for bad input, 404 for not found, 500 for server errors.
- Design your routes for one resource at a time (users, then orders, then products). The pattern is always the same — once you've built one RESTful resource, you can build them all.
Wrapping Up
REST isn't complicated — it's a set of conventions that make APIs predictable. Once you know the pattern (nouns for URLs, HTTP methods for actions, status codes for results), you can design any API. And the beauty is that every developer who knows REST can understand your API instantly, without documentation.
I'm learning all of this through the ChaiCode Web Dev Cohort 2026 under Hitesh Chaudhary and Piyush Garg. Building my first proper REST API was the moment backend development stopped being "writing server code" and started being "designing systems." It's a mindset shift, and once it clicks, every API you build will be clean, consistent, and professional.
Connect with me on LinkedIn or visit PrathamDEV.in. More articles on the way.
Happy coding! 🚀
Written by Pratham Bhardwaj | Web Dev Cohort 2026, ChaiCode
Top comments (0)