Chapter 26 — REST API Best Practices
📖 Definition
REST (Representational State Transfer) is an architectural style for APIs that uses HTTP verbs, resource-based URLs, and stateless interactions.
🔍 Core Rules
| Principle |
Description |
| Resource-based |
URLs name nouns, not actions |
| Stateless |
Each request carries everything needed |
| Uniform interface |
Use standard HTTP verbs + status codes |
| Cacheable |
Use Cache-Control, ETag |
| Layered |
Client → CDN → API gateway → service — each layer transparent |
🌐 HTTP Verbs → Operations
| Verb |
Operation |
Idempotent? |
Safe? |
GET |
Read |
✅ |
✅ |
POST |
Create / RPC |
❌ |
❌ |
PUT |
Replace |
✅ |
❌ |
PATCH |
Partial update |
❌ (depends) |
❌ |
DELETE |
Remove |
✅ |
❌ |
💻 Code Example — Good vs Bad URL Design
| ❌ Bad |
✅ Good |
/getUsers |
GET /users |
/createOrder |
POST /orders |
/deleteUser?id=5 |
DELETE /users/5 |
/users/5/getOrders |
GET /users/5/orders |
/users-list |
GET /users |
/UpdateUserStatusToActive |
PATCH /users/5 { status: "active" } |
📊 Common Status Codes
| Code |
Meaning |
When |
| 200 OK |
Success |
GET, PUT, PATCH, DELETE returning data |
| 201 Created |
Resource created |
POST |
| 204 No Content |
Success, empty body |
DELETE, sometimes PUT |
| 301 / 302 |
Redirect |
Moved |
| 400 Bad Request |
Validation failed |
Body / params invalid |
| 401 Unauthorized |
Missing / invalid auth |
No token |
| 403 Forbidden |
Authenticated but not allowed |
Wrong role |
| 404 Not Found |
Resource doesn't exist |
Bad id |
| 409 Conflict |
State conflict |
Email already exists |
| 422 Unprocessable |
Semantic validation failed |
Some APIs prefer this over 400 |
| 429 Too Many Requests |
Rate limit |
Throttled |
| 500 Internal Server Error |
Unhandled exception |
Bug |
| 503 Service Unavailable |
Down for maintenance |
Deploy / overload |
💻 Code Example — Express CRUD Routes
import express from "express";
import { User } from "./models/user.js";
const router = express.Router();
router.get("/", async (req, res) => {
const { page = 1, limit = 20 } = req.query;
const users = await User.find()
.skip((page - 1) * limit)
.limit(+limit);
res.json(users);
});
router.get("/:id", async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) return res.status(404).json({ error: "Not found" });
res.json(user);
});
router.post("/", async (req, res) => {
const user = await User.create(req.body);
res.status(201).json(user);
});
router.patch("/:id", async (req, res) => {
const user = await User.findByIdAndUpdate(req.params.id, req.body, { new: true });
if (!user) return res.status(404).json({ error: "Not found" });
res.json(user);
});
router.delete("/:id", async (req, res) => {
const result = await User.findByIdAndDelete(req.params.id);
if (!result) return res.status(404).json({ error: "Not found" });
res.status(204).end();
});
export default router;
💻 Code Example — Consistent Error Format
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Email is required",
"details": { "field": "email" }
}
}
class AppError extends Error {
constructor(status, code, message, details) {
super(message);
Object.assign(this, { status, code, details });
}
}
app.use((err, req, res, next) => {
res.status(err.status || 500).json({
error: {
code: err.code || "INTERNAL_ERROR",
message: err.message || "Internal Server Error",
...(err.details && { details: err.details }),
},
});
});
💻 Code Example — Pagination Patterns
GET /users?page=2&limit=20
GET /messages?after=63f0…&limit=20
💻 Code Example — Filtering & Sorting
GET /products?category=books&minPrice=100&maxPrice=500&sort=-createdAt
router.get("/products", async (req, res) => {
const { category, minPrice, maxPrice, sort = "-createdAt" } = req.query;
const query = {};
if (category) query.category = category;
if (minPrice || maxPrice) {
query.price = {};
if (minPrice) query.price.$gte = +minPrice;
if (maxPrice) query.price.$lte = +maxPrice;
}
const data = await Product.find(query).sort(sort).limit(50);
res.json(data);
});
💻 Code Example — Versioning
/api/v1/users
/api/v2/users
Or via header:
Accept: application/vnd.app.v2+json
URL versioning is simpler and more visible — preferred for most REST APIs.
💻 Code Example — Idempotency Key (Safe POSTs)
POST /orders
Idempotency-Key: f9d8…
async function createOrder(req, res) {
const key = req.headers["idempotency-key"];
if (!key) return res.status(400).json({ error: "Missing Idempotency-Key" });
const cached = await IdempotencyCache.findOne({ key });
if (cached) return res.json(cached.response);
const order = await Order.create(req.body);
await IdempotencyCache.create({ key, response: order });
res.status(201).json(order);
}
🌍 Real-World Impact
- Consistent APIs make your frontends faster to build.
- Status codes done right = fewer ambiguous bugs.
- Cursor pagination scales infinitely; offset breaks past page 1000+.
🎯 Likely Interview Questions
- What is REST?
- Difference between PUT and PATCH?
- Difference between 401 and 403?
- How do you version a REST API?
- What is idempotency? Which methods are idempotent?
← JWT | Index | Next: MVC Architecture →