Code Around Every Handler
Middleware is code that runs around your handlers: before the request reaches them, after the response leaves them, or instead of them. Logging, authentication, rate limiting, compression — all middleware.
The same idea travels under three names: web frameworks say middleware, gRPC says interceptor, some ecosystems say filter. One concept, three vocabularies.
In TypeScript, middleware is a function that takes a handler and returns a wrapped handler — exactly the Decorator pattern from the design patterns group, and a stack of them behaves like a Chain of Responsibility.
A Logging Middleware
withLogging wraps any handler: it prints the request coming in, lets the handler work, and prints again on the way out. The handler itself stays completely unaware.
Real logging usually adds a timestamp and a duration. These examples leave both out on purpose, so the printed output is the same on every run.
Wrap once, log everything
One wrapper around the whole router, and both paths get logged — the logging is written once, in the wrapper, not in every handler.
Output: TypeScript file and captured output
import { createServer, type IncomingMessage, type ServerResponse } from "node:http"
type Handler = (req: IncomingMessage, res: ServerResponse) => void
// Middleware: takes a handler, returns a wrapped handler.
function withLogging(next: Handler): Handler {
return (req, res) => {
console.log("-> ", req.method, req.url)
next(req, res)
console.log("<- done")
}
}
interface Route {
method: string
path: string
handler: Handler
}
function router(routes: Route[]): Handler {
return (req, res) => {
const url = new URL(req.url ?? "/", "http://localhost")
const route = routes.find(r => r.method === req.method && r.path === url.pathname)
if (route === undefined) {
res.writeHead(404).end("not found\n")
return
}
route.handler(req, res)
}
}
const routes: Route[] = [
{ method: "GET", path: "/users", handler: (req, res) => res.end("Ana, Ben\n") },
{ method: "GET", path: "/notes", handler: (req, res) => res.end("shopping, ideas\n") },
]
async function main(): Promise<void> {
// One wrapper around the whole router: every route is logged.
const server = createServer(withLogging(router(routes)))
await new Promise<void>(resolve => server.listen(0, () => resolve()))
const port = (server.address() as { port: number }).port
for (const path of ["/users", "/notes"]) {
const res = await fetch(`http://localhost:${port}${path}`)
console.log("body:", (await res.text()).trim())
}
server.close()
}
main()
Middleware That Stops the Request
Middleware can also refuse: an auth check that finds no valid key writes 401 Unauthorized and never calls the next handler.
Chained as withLogging(withAuth(handler)), the request passes through the layers in order — and the response bubbles back out through them.
An auth layer in the chain
The request without the X-API-Key header is stopped by the auth layer with 401 — the handler never runs. With the key, the request passes the whole chain and reaches the data.
Output: TypeScript file and captured output
import { createServer, type IncomingMessage, type ServerResponse } from "node:http"
type Handler = (req: IncomingMessage, res: ServerResponse) => void
function withLogging(next: Handler): Handler {
return (req, res) => {
console.log("->", req.method, req.url)
next(req, res)
}
}
// Auth middleware: it can stop the request before the handler runs.
function withAuth(next: Handler): Handler {
return (req, res) => {
if (req.headers["x-api-key"] !== "secret123") {
res.writeHead(401).end("missing or wrong API key\n")
return
}
next(req, res)
}
}
const report: Handler = (req, res) => {
res.end("the secret report\n")
}
async function main(): Promise<void> {
// The request passes through the layers in order.
const server = createServer(withLogging(withAuth(report)))
await new Promise<void>(resolve => server.listen(0, () => resolve()))
const port = (server.address() as { port: number }).port
for (const key of ["", "secret123"]) {
const headers: Record<string, string> = {}
if (key !== "") headers["X-API-Key"] = key
const res = await fetch(`http://localhost:${port}/report`, { headers })
console.log(" status:", res.status, "-", (await res.text()).trim())
}
server.close()
}
main()