Один обработчик на маршрут
Маршрутизация решает, какой обработчик отвечает на какой запрос. У модуля node:http нет собственного маршрутизатора: он передаёт каждый запрос в одну функцию, а вы решаете, куда его направить. Это занимает всего несколько строк — и, написав их один раз, вы увидите, что именно делает маршрутизатор.
Составные части — это метод запроса (req.method), его путь и его строка запроса. new URL(req.url ?? "/", "http://localhost") отделяет путь от строки запроса, потому что req.url — это только часть после хоста. Шаблон вроде "/users/{id}" затем сопоставляется сегмент за сегментом, и совпавшая часть передаётся обработчику как params.id. Фреймворки добавляют сверху удобства, но этого хватает для повседневных задач.
Маршруты и значения из пути
Два маршрута: список по адресу /users и один пользователь по /users/{id}. Часть в фигурных скобках — это параметр пути, тот кусок URL, который является данными.
Маршрут с параметром пути
Один и тот же маршрутизатор направляет /users в один обработчик, а /users/7 — в другой, и matchPath передаёт число 7 обработчику как params.id — сам обработчик не режет строки.
Вывод: программа на TypeScript и захваченный вывод
import { createServer, type IncomingMessage, type ServerResponse } from "node:http"
type RouteHandler = (
req: IncomingMessage,
res: ServerResponse,
params: Record<string, string>,
) => void
interface Route {
method: string
pattern: string
handler: RouteHandler
}
// Сопоставляет "/users/{id}" с "/users/7" и возвращает { id: "7" }.
// Возвращает undefined, когда шаблон не подходит к пути.
function matchPath(pattern: string, path: string): Record<string, string> | undefined {
const want = pattern.split("/")
const got = path.split("/")
if (want.length !== got.length) return undefined
const params: Record<string, string> = {}
for (let i = 0; i < want.length; i++) {
if (want[i].startsWith("{") && want[i].endsWith("}")) {
params[want[i].slice(1, -1)] = got[i]
} else if (want[i] !== got[i]) {
return undefined
}
}
return params
}
function router(routes: Route[]) {
return (req: IncomingMessage, res: ServerResponse): void => {
const url = new URL(req.url ?? "/", "http://localhost")
for (const route of routes) {
const params = matchPath(route.pattern, url.pathname)
if (params !== undefined && route.method === req.method) {
route.handler(req, res, params)
return
}
}
res.writeHead(404).end("не найдено\n")
}
}
const routes: Route[] = [
{
method: "GET",
pattern: "/users",
handler: (req, res) => {
res.end("все пользователи: Ana, Ben, Cleo\n")
},
},
{
method: "GET",
pattern: "/users/{id}",
handler: (req, res, params) => {
res.end(`один пользователь, id = ${params.id}\n`)
},
},
]
async function main(): Promise<void> {
const server = createServer(router(routes))
await new Promise<void>(resolve => server.listen(0, () => resolve()))
const port = (server.address() as { port: number }).port
for (const path of ["/users", "/users/7"]) {
const res = await fetch(`http://localhost:${port}${path}`)
process.stdout.write(`GET ${path} -> ` + (await res.text()))
}
server.close()
}
main()
Параметры запроса
Часть после ? содержит параметры запроса — настройки запроса, например текст поиска или ограничение: /search?q=api&limit=2. Они берутся из url.searchParams, всегда в виде строк, а get() возвращает null для отсутствующего параметра — поэтому ?? может подставить значение по умолчанию.
Правило простое: путь говорит, какой ресурс, а строка запроса говорит, как — фильтры, сортировка, постраничный вывод.
Чтение строки запроса
Обработчик читает q и limit из строки запроса. Во втором запросе limit пропущен, поэтому обработчик возвращается к значению 10 — отсутствующие параметры это нормально, учитывайте их.
Вывод: программа на TypeScript и захваченный вывод
import { createServer, type IncomingMessage, type ServerResponse } from "node:http"
function searchHandler(req: IncomingMessage, res: ServerResponse): void {
// req.url — это только путь и строка запроса, поэтому дадим URL базовый адрес.
const url = new URL(req.url ?? "/", "http://localhost")
// get() возвращает null, когда параметр отсутствует — ?? подставляет значение по умолчанию.
const q = url.searchParams.get("q") ?? ""
const limit = url.searchParams.get("limit") ?? "10"
res.end(`ищем ${q} с ограничением ${limit}\n`)
}
async function main(): Promise<void> {
const server = createServer((req, res) => {
const url = new URL(req.url ?? "/", "http://localhost")
if (req.method === "GET" && url.pathname === "/search") {
searchHandler(req, res)
return
}
res.writeHead(404).end("не найдено\n")
})
await new Promise<void>(resolve => server.listen(0, () => resolve()))
const port = (server.address() as { port: number }).port
for (const path of ["/search?q=api&limit=2", "/search?q=json"]) {
const res = await fetch(`http://localhost:${port}${path}`)
process.stdout.write(`GET ${path} -> ` + (await res.text()))
}
server.close()
}
main()