Код вокруг каждого обработчика
Middleware — это код, который выполняется вокруг ваших обработчиков: до того, как запрос дойдёт до них, после того, как ответ уйдёт от них, или вместо них. Логирование, аутентификация, ограничение частоты запросов, сжатие — всё это middleware.
Одна и та же идея встречается под тремя названиями: веб-фреймворки говорят middleware, gRPC говорит interceptor, некоторые экосистемы говорят filter. Одно понятие, три словаря.
В TypeScript middleware — это функция, которая принимает обработчик и возвращает обёрнутый обработчик, то есть в точности паттерн Decorator из группы паттернов проектирования, а стопка таких функций ведёт себя как Chain of Responsibility.
Middleware для логирования
withLogging оборачивает любой обработчик: печатает входящий запрос, даёт обработчику выполнить работу и печатает ещё раз на выходе. Сам обработчик при этом ни о чём не знает.
Настоящее логирование обычно добавляет метку времени и длительность. В этих примерах и то и другое намеренно опущено, чтобы печатаемый вывод был одинаковым при каждом запуске.
Обернуть один раз, логировать всё
Одна обёртка вокруг всего роутера — и оба пути логируются: логирование написано один раз, в обёртке, а не в каждом обработчике.
Вывод: программа на TypeScript и захваченный вывод
import { createServer, type IncomingMessage, type ServerResponse } from "node:http"
type Handler = (req: IncomingMessage, res: ServerResponse) => void
// Middleware: принимает обработчик и возвращает обёрнутый обработчик.
function withLogging(next: Handler): Handler {
return (req, res) => {
console.log("-> ", req.method, req.url)
next(req, res)
console.log("<- готово")
}
}
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("не найдено\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("покупки, идеи\n") },
]
async function main(): Promise<void> {
// Одна обёртка вокруг всего роутера: каждый маршрут логируется.
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("тело:", (await res.text()).trim())
}
server.close()
}
main()
Middleware, которое останавливает запрос
Middleware может также отказать: проверка аутентификации, не нашедшая корректного ключа, пишет 401 Unauthorized и никогда не вызывает следующий обработчик.
Собранный в цепочку как withLogging(withAuth(handler)), запрос проходит через слои по порядку, а ответ возвращается обратно через них.
Слой аутентификации в цепочке
Запрос без заголовка X-API-Key останавливается слоем аутентификации с кодом 401 — обработчик так и не выполняется. С ключом запрос проходит всю цепочку и добирается до данных.
Вывод: программа на TypeScript и захваченный вывод
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)
}
}
// Middleware аутентификации: может остановить запрос до того, как сработает обработчик.
function withAuth(next: Handler): Handler {
return (req, res) => {
if (req.headers["x-api-key"] !== "secret123") {
res.writeHead(401).end("ключ API отсутствует или неверен\n")
return
}
next(req, res)
}
}
const report: Handler = (req, res) => {
res.end("секретный отчёт\n")
}
async function main(): Promise<void> {
// Запрос проходит через слои по порядку.
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(" статус:", res.status, "-", (await res.text()).trim())
}
server.close()
}
main()