beginner
backend

Routing and Middleware

Master Express.js routing patterns and middleware architecture for building modular APIs

express fundamentals
20 min read

Advanced Routing in Express

Routing is the core of any web application. Express provides a powerful and flexible routing system that allows you to create clean, maintainable API endpoints.

Learning Objectives

  • Understand Express Router and modular routing
  • Learn route parameters and query strings
  • Master middleware types and patterns
  • Build reusable middleware functions

Express Router

The Express Router is a mini Express application capable of handling routes and middleware. It helps organize your routes into separate modules.

Creating a Router

javascript
// routes/products.js const express = require("express") const router = express.Router() // All routes here are relative to /api/products router.get("/", (req, res) => { res.json({ message: "Get all products" }) }) router.get("/:id", (req, res) => { res.json({ message: `Get product ${req.params.id}` }) }) router.post("/", (req, res) => { res.status(201).json({ message: "Create product" }) }) module.exports = router

Mounting Routers

javascript
// app.js const express = require("express") const productsRouter = require("./routes/products") const usersRouter = require("./routes/users") const ordersRouter = require("./routes/orders") const app = express() // Mount routers with prefixes app.use("/api/products", productsRouter) app.use("/api/users", usersRouter) app.use("/api/orders", ordersRouter) module.exports = app

Route Parameters

Basic Route Parameters
javascript
// Single parameter app.get('/users/:id', (req, res) => { const userId = req.params.id; res.json({ userId }); }); // Example: GET /users/123 // req.params = { id: '123' }

Query Strings

Query strings are used for filtering, pagination, and search:

javascript
// GET /api/products?category=electronics&sort=price&order=desc&page=1&limit=10 app.get("/api/products", (req, res) => { const { category, sort = "createdAt", order = "asc", page = 1, limit = 10 } = req.query console.log({ category, // 'electronics' sort, // 'price' order, // 'desc' page, // '1' (string!) limit, // '10' (string!) }) // Convert to numbers const pageNum = parseInt(page, 10) const limitNum = parseInt(limit, 10) const skip = (pageNum - 1) * limitNum // Build query... res.json({ data: [], pagination: { page: pageNum, limit: limitNum, total: 100, }, }) })

Middleware Deep Dive

Middleware functions are the heart of Express. They execute in the order they're defined and can:

  • Execute code
  • Modify request and response objects
  • End the request-response cycle
  • Call the next middleware

Types of Middleware


Creating Custom Middleware

Authentication Middleware

javascript
// middleware/auth.js const jwt = require("jsonwebtoken") const authenticate = (req, res, next) => { const authHeader = req.headers.authorization if (!authHeader || !authHeader.startsWith("Bearer ")) { return res.status(401).json({ error: "No token provided" }) } const token = authHeader.split(" ")[1] try { const decoded = jwt.verify(token, process.env.JWT_SECRET) req.user = decoded next() } catch (error) { return res.status(401).json({ error: "Invalid token" }) } } // Role-based authorization const authorize = (...roles) => { return (req, res, next) => { if (!roles.includes(req.user.role)) { return res.status(403).json({ error: "Not authorized to access this resource", }) } next() } } module.exports = { authenticate, authorize }

Request Validation Middleware

javascript
// middleware/validate.js const validateBody = (schema) => { return (req, res, next) => { const { error, value } = schema.validate(req.body) if (error) { return res.status(400).json({ error: "Validation error", details: error.details.map((d) => d.message), }) } req.body = value // Use validated/sanitized data next() } } // Usage with Joi const Joi = require("joi") const createUserSchema = Joi.object({ name: Joi.string().min(2).max(50).required(), email: Joi.string().email().required(), password: Joi.string().min(8).required(), }) router.post("/users", validateBody(createUserSchema), usersController.create)

Rate Limiting Middleware

javascript
// middleware/rateLimit.js const rateLimit = (options = {}) => { const { windowMs = 60000, // 1 minute max = 100, // requests per window message = "Too many requests", } = options const requests = new Map() return (req, res, next) => { const key = req.ip const now = Date.now() // Clean old entries const windowStart = now - windowMs if (!requests.has(key)) { requests.set(key, []) } const userRequests = requests.get(key).filter((time) => time > windowStart) if (userRequests.length >= max) { return res.status(429).json({ error: message }) } userRequests.push(now) requests.set(key, userRequests) next() } } // Usage app.use("/api", rateLimit({ max: 100, windowMs: 60000 }))

Middleware Execution Order

Middleware Chain

javascript
app.use(helmet()); // 1. Security headers app.use(cors()); // 2. CORS app.use(morgan('dev')); // 3. Logging app.use(express.json()); // 4. Parse body app.use(rateLimit()); // 5. Rate limiting // 6. Routes (with their own middleware) app.use('/api', authenticate, apiRoutes); app.use('/public', publicRoutes); // 7. 404 Handler app.use((req, res) => { res.status(404).json({ error: 'Not found' }); }); // 8. Error Handler (always last!) app.use((err, req, res, next) => { res.status(500).json({ error: err.message }); });

Route Chaining

Express allows you to chain route handlers:

javascript
// Method 1: Multiple callbacks app.get("/users/:id", validateParams, authenticate, authorize("admin"), async (req, res) => { // All middleware passed! res.json(user) }) // Method 2: router.route() for same path, different methods router.route("/users").get(getUsers).post(validateBody(userSchema), createUser) router.route("/users/:id").get(getUser).patch(validateBody(updateSchema), updateUser).delete(authorize("admin"), deleteUser)

Best Practices

✅ Routing & Middleware Best Practices
  1. Keep routes organized — Group related routes in separate files

  2. Use async/await with try-catch — Or use an async handler wrapper

  3. Validate input early — Use validation middleware before handlers

  4. Keep middleware focused — One middleware, one responsibility

  5. Order matters — Error handlers must be last

  6. Use meaningful HTTP status codes — 200, 201, 400, 401, 403, 404, 500


Next Steps

Continue Learning
Now that you understand routing and middleware, let's connect to a database and build real CRUD operations!

Next Lesson

Database Integration with MongoDB/PostgreSQL