Пошук уроків, статей та іншого контенту
Побудуємо маршрутизацію без фреймворків і зіставимо URL та HTTP-методи з обробниками запитів.
Маршрутизація — це зіставлення вхідного HTTP-запиту з обробником, який має виконати потрібну дію.
Для маршрутизації потрібно врахувати щонайменше:
HTTP-метод: GET, POST, PUT, DELETE тощо;
шлях URL: /users, /users/42;
параметри шляху: 42 у /users/42;
query-параметри: ?page=2;
обробник запиту.
У Node.js без фреймворків маршрутизацію можна побудувати поверх модуля node:http.
Об’єкт запиту IncomingMessage містить:
req.method — HTTP-метод;
req.url — URL разом із query-параметрами;
req.headers — HTTP-заголовки;
тіло запиту передається як потік даних.
Для надійного розбору URL зручно використовувати вбудований клас URL:
const url = new URL(req.url, `http://${req.headers.host}`);Після цього:
url.pathname; // "/users/42"
url.searchParams.get("page"); // значення параметра pageДругий аргумент конструктора потрібен тому, що req.url зазвичай містить відносний URL, а не повну адресу.
Маршрутизатор можна представити як масив маршрутів. Кожен маршрут містить:
HTTP-метод;
шаблон шляху;
функцію-обробник.
Наприклад:
const routes = [
{
method: "GET",
pattern: /^\/$/,
handler: homeHandler,
},
{
method: "GET",
pattern: /^\/users$/,
handler: usersHandler,
},
];Під час кожного запиту потрібно:
розібрати URL;
знайти маршрут із відповідним методом;
перевірити шлях;
викликати обробник;
повернути 404, якщо маршрут не знайдено.
Маршрут /users/42 і маршрут /users/17 мають однакову структуру, але різні значення параметра id.
Для зіставлення такого шляху можна використати регулярний вираз із іменованою групою:
/^\/users\/(?<id>[^/]+)$/Тоді для шляху /users/42 значення match.groups.id буде дорівнювати "42".
Параметри шляху та query-параметри мають різне призначення:
/users/42 — ідентифікатор ресурсу;
/users?page=2 — додаткові параметри запиту.
Створимо HTTP-сервер із такими маршрутами:
GET / — перевірка роботи сервера;
GET /users — отримання всіх користувачів;
GET /users/:id — отримання одного користувача;
POST /users — створення користувача.
const http = require("node:http");
const users = [
{ id: 1, name: "Olena" },
{ id: 2, name: "Andrii" },
];
function sendJson(res, statusCode, data) {
res.writeHead(statusCode, {
"Content-Type": "application/json; charset=utf-8",
});
res.end(JSON.stringify(data));
}
function readJsonBody(req) {
return new Promise((resolve, reject) => {
let body = "";
req.setEncoding("utf8");
req.on("data", (chunk) => {
body += chunk;
});
req.on("end", () => {
if (body.trim() === "") {
resolve({});
return;
}
try {
resolve(JSON.parse(body));
} catch {
reject(new Error("Некоректний JSON"));
}
});
req.on("error", reject);
});
}
function homeHandler(req, res) {
sendJson(res, 200, {
message: "API працює",
});
}
function listUsersHandler(req, res, params, url) {
const limit = Number(url.searchParams.get("limit"));
if (Number.isInteger(limit) && limit > 0) {
sendJson(res, 200, users.slice(0, limit));
return;
}
sendJson(res, 200, users);
}
function getUserHandler(req, res, params) {
const userId = Number(params.id);
const user = users.find((item) => item.id === userId);
if (!user) {
sendJson(res, 404, {
error: "Користувача не знайдено",
});
return;
}
sendJson(res, 200, user);
}
async function createUserHandler(req, res) {
let data;
try {
data = await readJsonBody(req);
} catch (error) {
sendJson(res, 400, {
error: error.message,
});
return;
}
if (typeof data.name !== "string" || data.name.trim() === "") {
sendJson(res, 400, {
error: "Поле name є обов'язковим",
});
return;
}
const user = {
id: users.length === 0 ? 1 : users[users.length - 1].id + 1,
name: data.name.trim(),
};
users.push(user);
sendJson(res, 201, user);
}
const routes = [
{
method: "GET",
pattern: /^\/$/,
handler: homeHandler,
},
{
method: "GET",
pattern: /^\/users\/?$/,
handler: listUsersHandler,
},
{
method: "GET",
pattern: /^\/users\/(?<id>[^/]+)\/?$/,
handler: getUserHandler,
},
{
method: "POST",
pattern: /^\/users\/?$/,
handler: createUserHandler,
},
];
function findRoute(method, pathname) {
for (const route of routes) {
if (route.method !== method) {
continue;
}
const match = pathname.match(route.pattern);
if (match) {
return {
route,
params: match.groups ?? {},
};
}
}
return null;
}
function getAllowedMethods(pathname) {
return [
...new Set(
routes
.filter((route) => route.pattern.test(pathname))
.map((route) => route.method),
),
];
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const { pathname } = url;
const matchedRoute = findRoute(req.method, pathname);
if (!matchedRoute) {
const allowedMethods = getAllowedMethods(pathname);
if (allowedMethods.length > 0) {
res.setHeader("Allow", allowedMethods.join(", "));
sendJson(res, 405, {
error: "Метод не дозволений для цього маршруту",
});
return;
}
sendJson(res, 404, {
error: "Маршрут не знайдено",
});
return;
}
try {
await matchedRoute.route.handler(
req,
res,
matchedRoute.params,
url,
);
} catch (error) {
console.error(error);
if (!res.headersSent) {
sendJson(res, 500, {
error: "Внутрішня помилка сервера",
});
}
}
});
const port = 3000;
server.listen(port, () => {
console.log(`Сервер запущено на http://localhost:${port}`);
});Збережіть код у файлі server.js і запустіть:
node server.jsПеревірити маршрути можна такими запитами:
curl http://localhost:3000/curl http://localhost:3000/userscurl "http://localhost:3000/users?limit=1"curl http://localhost:3000/users/2curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name":"Svitlana"}'Для запиту:
GET /users/2відбувається така послідовність:
req.method має значення GET.
url.pathname має значення /users/2.
findRoute перевіряє маршрути по черзі.
Метод маршруту порівнюється з методом запиту.
Регулярний вираз знаходить збіг.
Значення 2 потрапляє в params.id.
Викликається getUserHandler.
Обробник не повинен самостійно аналізувати весь URL. Його завдання — працювати з уже підготовленими параметрами.
404 і 405Ці статуси мають різне значення:
404 Not Found — такого шляху не існує;
405 Method Not Allowed — шлях існує, але для нього не підтримується вказаний метод.
Наприклад:
DELETE /usersШлях /users існує, але маршрут для DELETE не оголошений. У такому випадку сервер повертає 405 і заголовок:
Allow: GET, POSTДля шляху:
GET /ordersякщо жодного маршруту /orders немає, сервер повертає 404.
Порядок перевірки має значення. Маршрути перевіряються зверху вниз, тому більш конкретні маршрути варто розміщувати перед загальнішими.
Наприклад, якщо використовується параметризований маршрут:
/^\/users\/(?<id>[^/]+)$/він не повинен помилково перехоплювати шлях /users. Для цього точний маршрут /users має бути оголошений окремо, а регулярний вираз параметризованого маршруту має вимагати додатковий сегмент.
У невеликому сервері всі маршрути можуть бути в одному файлі. Але навіть тоді корисно розділяти відповідальність:
маршрутизатор визначає, який обробник викликати;
обробник читає параметри та виконує операцію;
функція відповіді формує статус, заголовки й тіло.
Таку структуру легше розширювати:
const routes = [
{
method: "GET",
pattern: /^\/products$/,
handler: listProductsHandler,
},
];Додавання маршруту не повинно вимагати змін у загальному коді сервера.
req.url як шляхуreq.url може містити query-параметри:
/users?limit=10Тому порівняння:
req.url === "/users"не знайде такий запит. Для маршрутизації використовуйте url.pathname.
Перевірка лише шляху:
if (pathname === "/users") {
// ...
}змішує GET /users, POST /users і DELETE /users. Метод має бути частиною умови зіставлення маршруту.
Кожен обробник повинен завершити відповідь через res.end() або функцію, яка його викликає. Якщо цього не зробити, клієнт може чекати без завершення запиту.
Тіло HTTP-запиту надходить частинами. Не можна припускати, що весь JSON доступний в одному фрагменті події data. Потрібно накопичити всі частини та викликати JSON.parse після події end.
Перед відправленням помилки перевіряйте res.headersSent. Якщо заголовки або відповідь уже були надіслані, повторне надсилання може спричинити помилку.
Маршрутизація зіставляє HTTP-метод і шлях URL з обробником.
Для розбору URL використовуйте URL, а для маршруту — url.pathname.
Query-параметри доступні через url.searchParams.
Динамічні сегменти можна витягувати регулярними виразами.
Для невідомого шляху повертають 404.
Для відомого шляху з непідтримуваним методом повертають 405.
Масив маршрутів і окремі обробники дають змогу побудувати простий маршрутизатор без фреймворків.