API DesignRESTful API Design & Routing
Imagine you are handed the keys to an unfamiliar city's subway system. With no map, no labels, and no conventions — navigating it is a nightmare. Now imagine a system where every line, every station, and every transfer follows a predictable, internationally agreed-upon pattern. That's what REST does for APIs.
REST (Representational State Transfer) is an architectural style for designing APIs. It isn't a library you install or a law enforced by your compiler — it's a set of conventions that the entire software industry follows so that APIs are instantly readable and predictable to any developer in the world.
The CRUD / HTTP Method Mapping
Almost everything that happens in software maps to four fundamental operations: Create, Read, Update, and Delete — collectively called CRUD. RESTful APIs map these directly to HTTP methods:
✨Create → POSTCreate a new resource. Returns 201 Created on success.
👁️Read → GETFetch a resource or list. Returns 200 OK. Never modifies data.
✏️Update → PUT/PATCHPUT replaces entirely. PATCH applies partial changes. Returns 200 OK.
🗑️Delete → DELETERemove a resource. Returns 200 OK or 204 No Content.
Rule 1 — URLs Are Nouns, Methods Are Verbs
This is the most fundamental rule in REST and the most commonly broken by beginners. The URL identifies what you are acting on (the resource — a noun). The HTTP method defines what action is being taken (the verb). Never put actions in your URLs.
❌ BAD — Action-based URLs (the wrong way)GET /getAllUsers
POST /createNewUser
POST /updateUser?id=5
GET /deleteUser/5
✅ GOOD — RESTful resource-based URLsGET /users → Fetch ALL users
POST /users → CREATE a new user
GET /users/5 → Fetch user with ID 5
PATCH /users/5 → UPDATE user ID 5
DELETE /users/5 → DELETE user ID 5
Notice how the same URL /users/5 means three different things depending entirely on the HTTP method. This is elegant, predictable, and the core insight of REST.
Nested Resources
In real applications, resources belong to other resources. A post belongs to a user. A comment belongs to a post. RESTful URLs express this hierarchy using nesting:
✅ Nested Resource URLsGET /users/5/posts → All posts by user 5
POST /users/5/posts → Create a post for user 5
GET /users/5/posts/12 → Specific post 12 by user 5
DELETE /users/5/posts/12 → Delete post 12 by user 5
GET /users/5/posts/12/comments → All comments on post 12
⚠️ Nesting Depth Warning: Never nest deeper than two levels (e.g., /users/:id/posts/:postId). Three or more levels creates URLs that are extremely hard to read and remember. Beyond two levels, restructure your resources to be addressable at the top level.
Dynamic Routing & Path Parameters in Express
You will never create a separate route for every single user. Instead, you define one dynamic route using a colon (:) prefix to create a path parameter that captures whatever value appears in that position of the URL.
const express = require('express');
const app = express();
app.use(express.json());
const products = [
{ id: 1, name: 'MacBook Pro', price: 2499, category: 'laptops' },
{ id: 2, name: 'iPhone 15', price: 999, category: 'phones' },
{ id: 3, name: 'iPad Air', price: 749, category: 'tablets' }
];
app.get('/products/:productId', (req, res) => {
const id = parseInt(req.params.productId);
if (isNaN(id)) {
return res.status(400).json({ error: 'Product ID must be a number' });
}
const product = products.find(p => p.id === id);
if (!product) {
return res.status(404).json({
error: `Product with ID ${id} does not exist`
});
}
res.status(200).json(product);
});
app.get('/categories/:categoryName/products/:productId', (req, res) => {
const { categoryName, productId } = req.params;
res.json({ category: categoryName, productId });
});
app.listen(3000);
Dynamic Routing in Flask
Flask uses angle-bracket syntax <variable> inside the route string, and Flask automatically passes the captured value as an argument to your function:
from flask import Flask, jsonify, abort
app = Flask(__name__)
products = [
{'id': 1, 'name': 'MacBook Pro', 'price': 2499},
{'id': 2, 'name': 'iPhone 15', 'price': 999},
]
@app.route('/products/', methods=['GET'])
def get_product(product_id):
product = next((p for p in products if p['id'] == product_id), None)
if product is None:
abort(404, description=f'Product {product_id} not found')
return jsonify(product), 200
if __name__ == '__main__':
app.run(debug=True)
Query Parameters — Filtering, Sorting, Paginating
While path parameters identify specific individual resources, query parameters modify how a collection is retrieved. They appear after a ? in the URL, as key-value pairs separated by &.
For example: GET /products?category=laptops&sort=price&order=asc&page=1&limit=10
app.get('/products', (req, res) => {
let result = [...products];
const { category, sort, order, page, limit } = req.query;
if (category) {
result = result.filter(p => p.category === category);
}
if (sort === 'price') {
result.sort((a, b) => order === 'desc' ? b.price - a.price : a.price - b.price);
}
const pageNum = parseInt(page) || 1;
const pageSize = parseInt(limit) || 10;
const startIdx = (pageNum - 1) * pageSize;
const paginated = result.slice(startIdx, startIdx + pageSize);
res.json({
total: result.length,
page: pageNum,
perPage: pageSize,
results: paginated
});
});
💡Always provide defaults: Query parameters are optional by nature. If a user doesn't provide page, don't crash — default to page 1. If they don't provide limit, default to something sensible like 10 or 20. Never return 50,000 rows at once.
The Complete CRUD Implementation
Here is a complete, properly structured RESTful implementation for a /tasks resource with all five operations:
const express = require('express');
const app = express();
app.use(express.json());
let tasks = [
{ id: 1, title: 'Learn Node.js', done: true },
{ id: 2, title: 'Build an API', done: false },
{ id: 3, title: 'Deploy to cloud', done: false }
];
let nextId = 4;
app.get('/tasks', (req, res) => {
let result = tasks;
if (req.query.done !== undefined) {
result = tasks.filter(t => t.done === (req.query.done === 'true'));
}
res.json({ count: result.length, tasks: result });
});
app.get('/tasks/:id', (req, res) => {
const task = tasks.find(t => t.id === parseInt(req.params.id));
if (!task) return res.status(404).json({ error: 'Task not found' });
res.json(task);
});
app.post('/tasks', (req, res) => {
const { title } = req.body;
if (!title?.trim()) {
return res.status(400).json({ error: 'title is required' });
}
const task = { id: nextId++, title: title.trim(), done: false };
tasks.push(task);
res.status(201).json({ message: 'Task created', task });
});
app.patch('/tasks/:id', (req, res) => {
const idx = tasks.findIndex(t => t.id === parseInt(req.params.id));
if (idx === -1) return res.status(404).json({ error: 'Task not found' });
tasks[idx] = { ...tasks[idx], ...req.body };
res.json({ message: 'Task updated', task: tasks[idx] });
});
app.delete('/tasks/:id', (req, res) => {
const idx = tasks.findIndex(t => t.id === parseInt(req.params.id));
if (idx === -1) return res.status(404).json({ error: 'Task not found' });
tasks.splice(idx, 1);
res.status(204).send();
});
app.listen(3000, () => console.log('Tasks API running'));
Express Router — Organizing Routes into Files
As your API grows, putting all routes in a single server.js becomes unmaintainable. Express provides Router to split routes into separate module files:
// routes/tasks.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => { /* GET all tasks */ });
router.post('/', (req, res) => { /* Create task */ });
router.get('/:id', (req, res) => { /* Get one task */ });
module.exports = router;
// server.js
const express = require('express');
const taskRoutes = require('./routes/tasks');
const app = express();
app.use(express.json());
app.use('/tasks', taskRoutes);
app.listen(3000);