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
| Method | Purpose | Idempotent |
|---|---|---|
| GET | Retrieve a resource | Yes |
| POST | Create a new resource | No |
| PUT | Replace a resource entirely | Yes |
| PATCH | Partially update a resource | Yes |
| DELETE | Remove a resource | Yes |
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 → Responseconst 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 corsconst 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).