Skip to main content
Node.js & Express·Lesson 3 of 5

Routing and Middleware

Routing determines how your application responds to client requests. Middleware functions are the backbone of Express — they process requests at every stage of the pipeline.

Route Methods

Express provides methods for every HTTP verb:

const express = require("express");
const router = express.Router();

router.get("/posts", (req, res) => {
  res.json({ action: "List all posts" });
});

router.post("/posts", (req, res) => {
  res.status(201).json({ action: "Create a post" });
});

router.put("/posts/:id", (req, res) => {
  res.json({ action: `Replace post ${req.params.id}` });
});

router.patch("/posts/:id", (req, res) => {
  res.json({ action: `Update post ${req.params.id}` });
});

router.delete("/posts/:id", (req, res) => {
  res.json({ action: `Delete post ${req.params.id}` });
});

HTTP Methods at a Glance

MethodPurposeIdempotent
GETRetrieve a resourceYes
POSTCreate a new resourceNo
PUTReplace a resource entirelyYes
PATCHPartially update a resourceYes
DELETERemove a resourceYes

Route Parameters

Capture dynamic values from the URL path:

// Single parameter
router.get("/users/:id", (req, res) => {
  res.json({ userId: req.params.id });
});

// Multiple parameters
router.get("/users/:userId/posts/:postId", (req, res) => {
  const { userId, postId } = req.params;
  res.json({ userId, postId });
});

Query Parameters

Query parameters come after ? in the URL:

// GET /search?q=javascript&page=2&limit=10
router.get("/search", (req, res) => {
  const { q, page = 1, limit = 20 } = req.query;

  res.json({
    query: q,
    page: parseInt(page),
    limit: parseInt(limit),
  });
});

Route Chaining with route()

Group different methods for the same path:

router
  .route("/articles")
  .get((req, res) => {
    res.json({ action: "List articles" });
  })
  .post((req, res) => {
    res.status(201).json({ action: "Create article" });
  });

router
  .route("/articles/:id")
  .get((req, res) => {
    res.json({ action: `Get article ${req.params.id}` });
  })
  .put((req, res) => {
    res.json({ action: `Update article ${req.params.id}` });
  })
  .delete((req, res) => {
    res.json({ action: `Delete article ${req.params.id}` });
  });

What is Middleware?

Middleware functions have access to the request object, response object, and the next function. They can execute code, modify the request/response, end the request-response cycle, or call next() to pass control to the next middleware.

function myMiddleware(req, res, next) {
  // Do something with req or res
  console.log(`${req.method} ${req.path}`);

  // Pass control to the next middleware
  next();
}

The Middleware Pipeline

Requests flow through middleware in the order they are registered:

Request  Logger  Auth  Route Handler  Error Handler  Response
const app = express();

// 1. Runs first for every request
app.use(express.json());

// 2. Runs second for every request
app.use(logger);

// 3. Runs for matching routes only
app.use("/api/admin", authMiddleware);

// 4. Route handlers
app.use("/api/users", userRoutes);

// 5. 404 handler (no matching route)
app.use(notFoundHandler);

// 6. Error handler (4 arguments)
app.use(errorHandler);

Building Custom Middleware

Request Logger

function logger(req, res, next) {
  const start = Date.now();

  res.on("finish", () => {
    const duration = Date.now() - start;
    console.log(
      `${req.method} ${req.originalUrl} ${res.statusCode} - ${duration}ms`
    );
  });

  next();
}

app.use(logger);

Request Validator

function validateBody(requiredFields) {
  return (req, res, next) => {
    const missing = requiredFields.filter((field) => !req.body[field]);

    if (missing.length > 0) {
      return res.status(400).json({
        error: "Missing required fields",
        fields: missing,
      });
    }

    next();
  };
}

router.post("/users", validateBody(["name", "email"]), (req, res) => {
  res.status(201).json({ user: req.body });
});

Rate Limiter

function rateLimit(maxRequests, windowMs) {
  const clients = new Map();

  return (req, res, next) => {
    const ip = req.ip;
    const now = Date.now();
    const windowStart = now - windowMs;

    if (!clients.has(ip)) {
      clients.set(ip, []);
    }

    const requests = clients.get(ip).filter((time) => time > windowStart);
    requests.push(now);
    clients.set(ip, requests);

    if (requests.length > maxRequests) {
      return res.status(429).json({
        error: "Too many requests",
        retryAfter: Math.ceil(windowMs / 1000),
      });
    }

    res.set("X-RateLimit-Limit", maxRequests);
    res.set("X-RateLimit-Remaining", maxRequests - requests.length);
    next();
  };
}

app.use("/api", rateLimit(100, 60 * 1000)); // 100 requests per minute

CORS Middleware

Cross-Origin Resource Sharing allows your API to be called from different domains:

npm install cors
const cors = require("cors");

// Allow all origins
app.use(cors());

// Or configure specifically
app.use(
  cors({
    origin: ["https://sabaoon.dev", "http://localhost:3000"],
    methods: ["GET", "POST", "PUT", "DELETE"],
    allowedHeaders: ["Content-Type", "Authorization"],
  })
);

Serving Static Files

const path = require("path");

app.use(express.static(path.join(__dirname, "public")));

// With a URL prefix
app.use("/assets", express.static(path.join(__dirname, "public")));

Files in the public directory become accessible at the root URL (or under /assets with the prefix).

Error Handling Middleware

Error middleware has four parameters — Express identifies it by the (err, req, res, next) signature:

// Async wrapper to catch promise rejections
function asyncHandler(fn) {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

// Use it with async route handlers
router.get(
  "/users/:id",
  asyncHandler(async (req, res) => {
    const user = await findUser(req.params.id);
    if (!user) {
      const error = new Error("User not found");
      error.statusCode = 404;
      throw error;
    }
    res.json(user);
  })
);

Practical Exercise

Build a middleware stack for a production API:

const express = require("express");
const cors = require("cors");
const app = express();

// Middleware stack
app.use(cors());
app.use(express.json({ limit: "10kb" }));
app.use(logger);
app.use("/api", rateLimit(100, 60 * 1000));

// Health check
app.get("/health", (req, res) => {
  res.json({ status: "healthy" });
});

// API routes
app.use("/api/users", userRoutes);

// 404
app.use((req, res) => {
  res.status(404).json({ error: "Not found" });
});

// Error handler
app.use((err, req, res, next) => {
  const status = err.statusCode || 500;
  res.status(status).json({ error: err.message });
});

Key Takeaways

  • Express routes match HTTP methods and URL patterns to handler functions.
  • Middleware functions form a pipeline — each can modify the request/response or pass control with next().
  • Register middleware in order: parsers first, then loggers, auth, routes, and error handlers last.
  • Custom middleware like validators and rate limiters keep your route handlers clean.
  • Error-handling middleware must have exactly four parameters: (err, req, res, next).