Topic 6 of 55
REST API Design
Overview
REST (Representational State Transfer) is the standard architectural style for building web APIs. Understanding REST conventions — proper HTTP methods, status codes, and URL design — is tested in virtually every backend interview.
Syntax
bash
/* RESTful URL conventions:
GET /products → list all products
POST /products → create a product
GET /products/:id → get one product
PUT /products/:id → replace entire product
PATCH /products/:id → partial update
DELETE /products/:id → delete product
Nested resources:
GET /users/:id/orders → orders for a user
POST /users/:id/orders → create order for user
HTTP Status Codes:
200 OK → successful GET, PUT, PATCH
201 Created → successful POST
204 No Content → successful DELETE
400 Bad Request → validation error
401 Unauthorized → not authenticated
403 Forbidden → authenticated but not authorized
404 Not Found → resource doesn't exist
409 Conflict → duplicate resource
422 Unprocessable → semantic validation error
429 Too Many Req → rate limited
500 Internal Error → server bug
*/Common Pitfalls
- PUT replaces the ENTIRE resource (missing fields become null/default); PATCH updates only specified fields.
- API versioning (/api/v1/) from day one — it lets you make breaking changes without breaking clients.
- Interview tip: REST is stateless — each request must contain all information needed to process it. No session on the server.
Real-World Example
A fully RESTful orders API with proper conventions:
example
bash
// GET /api/v1/orders?status=pending&page=1&limit=20
router.get('/', async (req, res, next) => {
try {
const { status, page = 1, limit = 20, sort = 'created_at:desc' } = req.query;
const [field, direction] = sort.split(':');
const { data, total } = await OrderService.findAll({
userId: req.user.id,
filters: { status },
pagination: { page: +page, limit: +limit },
sort: { field, direction },
});
res.json({
data,
pagination: {
page: +page,
limit: +limit,
total,
totalPages: Math.ceil(total / limit),
hasNext: page * limit < total,
},
_links: {
self: `/api/v1/orders?page=${page}&limit=${limit}`,
next: page * limit < total ? `/api/v1/orders?page=${+page+1}&limit=${limit}` : null,
}
});
} catch (error) { next(error); }
});
// PATCH /api/v1/orders/:id — partial update
router.patch('/:id', authenticate, async (req, res, next) => {
try {
const order = await OrderService.findById(req.params.id);
if (!order) return res.status(404).json({ error: 'Order not found' });
if (order.userId !== req.user.id) return res.status(403).json({ error: 'Forbidden' });
const updated = await OrderService.update(req.params.id, req.body);
res.json(updated);
} catch (error) { next(error); }
});