RESTful API Design
Checking access...
REST (Representational State Transfer) is an architectural style for designing networked APIs. This page covers how to build clean, consistent, and scalable REST APIs with Express.
REST Principles
- Resources: Everything is a resource (
/users,/products,/orders) - HTTP methods: Actions on resources use standard HTTP methods
- Stateless: Each request contains all information needed
- Uniform interface: Consistent URL patterns and response formats
- Resource representations: JSON, XML, etc.
CRUD Endpoint Pattern
| Action | HTTP Method | Endpoint | Status Code |
|---|---|---|---|
| Create | POST | /api/resources | 201 |
| Read (all) | GET | /api/resources | 200 |
| Read (one) | GET | /api/resources/:id | 200 |
| Update (full) | PUT | /api/resources/:id | 200 |
| Update (partial) | PATCH | /api/resources/:id | 200 |
| Delete | DELETE | /api/resources/:id | 204 |
Full REST Controller Example
let tasks = [];let nextId = 1;
// GET /api/tasksexports.getAll = (req, res) => { let result = [...tasks];
// Filtering const { status, priority, search } = req.query; if (status) { result = result.filter((t) => t.status === status); } if (priority) { result = result.filter((t) => t.priority === priority); } if (search) { result = result.filter((t) => t.title.toLowerCase().includes(search.toLowerCase()) ); }
// Sorting const { sortBy = "createdAt", order = "desc" } = req.query; result.sort((a, b) => { const cmp = a[sortBy] > b[sortBy] ? 1 : -1; return order === "asc" ? cmp : -cmp; });
// Pagination const page = parseInt(req.query.page) || 1; const limit = parseInt(req.query.limit) || 10; const startIndex = (page - 1) * limit; const paginatedResult = result.slice(startIndex, startIndex + limit);
res.json({ data: paginatedResult, pagination: { page, limit, total: result.length, totalPages: Math.ceil(result.length / limit), }, });};
// GET /api/tasks/:idexports.getById = (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({ data: task });};
// POST /api/tasksexports.create = (req, res) => { const { title, description, priority = "medium" } = req.body;
const task = { id: nextId++, title, description: description || "", status: "pending", priority, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), };
tasks.push(task); res.status(201).json({ data: task });};
// PUT /api/tasks/:idexports.update = (req, res) => { const task = tasks.find((t) => t.id === parseInt(req.params.id));
if (!task) { return res.status(404).json({ error: "Task not found" }); }
const { title, description, status, priority } = req.body;
Object.assign(task, { title: title || task.title, description: description !== undefined ? description : task.description, status: status || task.status, priority: priority || task.priority, updatedAt: new Date().toISOString(), });
res.json({ data: task });};
// PATCH /api/tasks/:idexports.patch = (req, res) => { const task = tasks.find((t) => t.id === parseInt(req.params.id));
if (!task) { return res.status(404).json({ error: "Task not found" }); }
// Only update provided fields Object.keys(req.body).forEach((key) => { if (key !== "id" && key !== "createdAt") { task[key] = req.body[key]; } });
task.updatedAt = new Date().toISOString(); res.json({ data: task });};
// DELETE /api/tasks/:idexports.remove = (req, res) => { const index = tasks.findIndex((t) => t.id === parseInt(req.params.id));
if (index === -1) { return res.status(404).json({ error: "Task not found" }); }
tasks.splice(index, 1); res.status(204).send();};Task Routes
const express = require("express");const router = express.Router();const taskController = require("../controllers/taskController");
router.get("/", taskController.getAll);router.get("/:id", taskController.getById);router.post("/", taskController.create);router.put("/:id", taskController.update);router.patch("/:id", taskController.patch);router.delete("/:id", taskController.remove);
module.exports = router;HTTP Status Codes
Success Codes
| Code | Meaning | When to Use |
|---|---|---|
| 200 | OK | GET, PUT, PATCH succeed |
| 201 | Created | POST creates a resource |
| 204 | No Content | DELETE succeeds (no body) |
Client Error Codes
| Code | Meaning | When to Use |
|---|---|---|
| 400 | Bad Request | Invalid input, missing fields |
| 401 | Unauthorized | Missing/invalid authentication |
| 403 | Forbidden | Authenticated but no permission |
| 404 | Not Found | Resource doesn’t exist |
| 409 | Conflict | Duplicate resource, version conflict |
| 422 | Unprocessable Entity | Validation failed |
| 429 | Too Many Requests | Rate limit exceeded |
Server Error Codes
| Code | Meaning | When to Use |
|---|---|---|
| 500 | Internal Server Error | Unexpected server error |
| 502 | Bad Gateway | Upstream service failed |
| 503 | Service Unavailable | Server overloaded or down |
Consistent Response Format
// Success response{ "data": { ... }, "pagination": { "page": 1, "limit": 10, "total": 42, "totalPages": 5 }}
// Single resource{ "data": { "id": 1, "name": "Alice" }}
// Collection{ "data": [ { "id": 1, "name": "Alice" }, { "id": 2, "name": "Bob" } ]}
// Error response{ "error": { "code": "VALIDATION_ERROR", "message": "Email is required", "details": [ { "field": "email", "message": "Must be a valid email address" } ] }}Request Validation
Manual Validation
const validateCreateTask = (req, res, next) => { const errors = [];
if (!req.body.title || req.body.title.trim().length < 3) { errors.push({ field: "title", message: "Title must be at least 3 characters", }); }
if (req.body.priority && !["low", "medium", "high"].includes(req.body.priority)) { errors.push({ field: "priority", message: "Priority must be low, medium, or high", }); }
if (errors.length > 0) { return res.status(422).json({ error: { code: "VALIDATION_ERROR", details: errors } }); }
next();};
router.post("/", validateCreateTask, taskController.create);Validation with express-validator
npm install express-validatorconst { body, validationResult } = require("express-validator");
const validateUser = [ body("name").trim().notEmpty().withMessage("Name is required"), body("email").isEmail().withMessage("Valid email is required"), body("age").optional().isInt({ min: 18 }).withMessage("Must be 18+"),];
const handleValidation = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(422).json({ error: { code: "VALIDATION_ERROR", details: errors.array().map((e) => ({ field: e.path, message: e.msg, })), }, }); } next();};
router.post("/users", validateUser, handleValidation, userController.create);Pagination
// Request: GET /api/tasks?page=2&limit=20&sortBy=createdAt&order=desc// Response:{ "data": [...], "pagination": { "page": 2, "limit": 20, "total": 95, "totalPages": 5 }}Pagination Middleware
const paginate = (model) => (req, res, next) => { const page = parseInt(req.query.page) || 1; const limit = parseInt(req.query.limit) || 10; const startIndex = (page - 1) * limit;
req.pagination = { page, limit, startIndex }; req.model = model; next();};API Versioning
// URL-based versioning (most common)app.use("/api/v1/users", v1UserRoutes);app.use("/api/v2/users", v2UserRoutes);
// Header-based versioningapp.use("/api/users", (req, res, next) => { const version = req.headers["accept-version"]; req.apiVersion = version || "1"; next();});API Documentation with Swagger
npm install swagger-jsdoc swagger-ui-expressconst swaggerJsdoc = require("swagger-jsdoc");const swaggerUi = require("swagger-ui-express");
const options = { definition: { openapi: "3.0.0", info: { title: "Task API", version: "1.0.0", description: "A RESTful API for managing tasks", }, }, apis: ["./src/routes/*.js"],};
const specs = swaggerJsdoc(options);app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(specs));Quick Reference
// RESTful endpointsGET /api/tasks // List (with filters, pagination)GET /api/tasks/:id // SinglePOST /api/tasks // CreatePUT /api/tasks/:id // ReplacePATCH /api/tasks/:id // Partial updateDELETE /api/tasks/:id // Delete
// Status codesres.status(200).json(data) // OKres.status(201).json(data) // Createdres.status(204).send() // No Contentres.status(400).json(error) // Bad Requestres.status(404).json(error) // Not Foundres.status(422).json(error) // Validation Errorres.status(500).json(error) // Server ErrorPractice Exercises
RESTful products API: Build a complete CRUD API for products with fields:
name,price,category,inStock. Include filtering by category, sorting by price, and pagination.Nested resources: Create
/api/users/:userId/ordersand/api/users/:userId/orders/:orderId/itemswith full CRUD. Validate that a user exists before accepting orders.Validation with express-validator: Add input validation to a registration endpoint. Validate: name (2-50 chars), email (valid format), password (min 8 chars, must contain number), age (optional, 13-120).