Пошук уроків, статей та іншого контенту
Розберете патерн middleware та створите проміжні обробники для логування, автентифікації й обробки запитів.
Middleware — це проміжний обробник, який отримує HTTP-запит, HTTP-відповідь і функцію переходу до наступного обробника.
Зазвичай middleware має такий вигляд:
async function middleware(req, res, next) {
// Дії до наступного обробника
await next();
// Дії після наступного обробника
}Middleware може:
змінити req або res;
перевірити умови запиту;
завершити відповідь і не передавати керування далі;
викликати next(), щоб продовжити обробку;
виконати дії після завершення наступних middleware.
Патерн middleware дає змогу розділити обробку запиту на невеликі незалежні частини. Наприклад, логування, автентифікацію, читання тіла запиту та виконання бізнес-логіки можна реалізувати окремими функціями.
Уявімо, що для запиту потрібно виконати такі кроки:
Записати інформацію про запит у журнал.
Прочитати JSON із тіла запиту.
Перевірити API-ключ.
Виконати основний обробник маршруту.
Ці кроки утворюють ланцюжок:
Запит
↓
Логування
↓
Читання тіла
↓
Автентифікація
↓
Обробник маршруту
↓
ВідповідьMiddleware викликає next(), коли поточна перевірка або підготовка завершена. Якщо middleware не викликає next(), наступні обробники не виконуються.
Це корисно, наприклад, для автентифікації:
async function authMiddleware(req, res, next) {
if (!req.headers["x-api-key"]) {
res.statusCode = 401;
res.end("Unauthorized");
return;
}
await next();
}У цьому прикладі запит без заголовка x-api-key завершується одразу. До наступного middleware керування не переходить.
У Node.js немає вбудованої функції use, яка автоматично створює ланцюжок middleware для звичайного http.Server. Такий механізм можна реалізувати самостійно.
Функція compose прийматиме масив обробників і повертатиме функцію для запуску цього ланцюжка:
function compose(middlewares) {
return function run(req, res) {
let currentIndex = -1;
function dispatch(index) {
if (index <= currentIndex) {
return Promise.reject(
new Error("next() викликано більше одного разу")
);
}
currentIndex = index;
const middleware = middlewares[index];
if (!middleware) {
return Promise.resolve();
}
try {
return Promise.resolve(
middleware(req, res, () => dispatch(index + 1))
);
} catch (error) {
return Promise.reject(error);
}
}
return dispatch(0);
};
}dispatch запускає middleware за індексом. Функція next фактично є скороченням для запуску наступного елемента масиву.
Middleware можуть бути асинхронними, тому кожен результат обгортається в Promise.resolve.
Middleware логування зазвичай виконує дії до та після next():
async function loggerMiddleware(req, res, next) {
const startedAt = Date.now();
try {
await next();
} catch (error) {
// Встановлюємо статус помилки до завершення журналювання
if (!res.headersSent) {
res.statusCode = 500;
}
throw error;
} finally {
const duration = Date.now() - startedAt;
console.log(
`${req.method} ${req.url} ${res.statusCode} — ${duration} мс`
);
}
}Виклик await next() важливий: він дозволяє дочекатися виконання всіх наступних middleware. Завдяки цьому можна виміряти повний час обробки запиту.
Блок finally виконається і в разі успішної відповіді, і в разі помилки.
У Node.js тіло HTTP-запиту надходить як потік даних. Його потрібно зібрати з частин, а потім перетворити на JavaScript-об’єкт.
function readRequestBody(req) {
return new Promise((resolve, reject) => {
let data = "";
req.on("data", (chunk) => {
data += chunk;
});
req.on("end", () => {
resolve(data);
});
req.on("error", reject);
});
}На основі цієї функції можна створити middleware для JSON:
async function jsonBodyMiddleware(req, res, next) {
const methodsWithBody = ["POST", "PUT", "PATCH"];
if (!methodsWithBody.includes(req.method)) {
await next();
return;
}
const contentType = req.headers["content-type"] || "";
if (!contentType.includes("application/json")) {
await next();
return;
}
const rawBody = await readRequestBody(req);
try {
req.body = rawBody ? JSON.parse(rawBody) : {};
} catch {
res.statusCode = 400;
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify({ error: "Некоректний JSON" }));
return;
}
await next();
}Після виконання цього middleware наступні обробники можуть звертатися до req.body.
Наприклад, для такого запиту:
{
"name": "Ada"
}об’єкт req.body матиме значення:
{
name: "Ada"
}Для прикладу використаємо API-ключ у заголовку x-api-key.
async function authMiddleware(req, res, next) {
const apiKey = req.headers["x-api-key"];
if (apiKey !== "dev-secret") {
res.statusCode = 401;
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify({ error: "Потрібна автентифікація" }));
return;
}
req.user = {
id: 1,
name: "Demo user"
};
await next();
}Після успішної перевірки middleware додає користувача до об’єкта запиту. Наступні обробники можуть використовувати req.user.
У реальному застосунку API-ключ не варто зберігати безпосередньо в коді. Тут значення "dev-secret" використовується лише для демонстрації патерну.
Нижче наведено повністю робочий приклад без зовнішніх бібліотек. Він містить:
ланцюжок middleware;
логування;
читання JSON;
автентифікацію;
обробник маршруту;
обробку помилок.
const http = require("node:http");
function compose(middlewares) {
return function run(req, res) {
let currentIndex = -1;
function dispatch(index) {
if (index <= currentIndex) {
return Promise.reject(
new Error("next() викликано більше одного разу")
);
}
currentIndex = index;
const middleware = middlewares[index];
if (!middleware) {
return Promise.resolve();
}
try {
return Promise.resolve(
middleware(req, res, () => dispatch(index + 1))
);
} catch (error) {
return Promise.reject(error);
}
}
return dispatch(0);
};
}
function sendJson(res, statusCode, payload) {
res.statusCode = statusCode;
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify(payload));
}
function readRequestBody(req) {
return new Promise((resolve, reject) => {
let data = "";
req.on("data", (chunk) => {
data += chunk;
});
req.on("end", () => {
resolve(data);
});
req.on("error", reject);
});
}
async function loggerMiddleware(req, res, next) {
const startedAt = Date.now();
try {
await next();
} catch (error) {
// Встановлюємо статус помилки до завершення журналювання
if (!res.headersSent) {
res.statusCode = 500;
}
throw error;
} finally {
const duration = Date.now() - startedAt;
console.log(
`${req.method} ${req.url} ${res.statusCode} — ${duration} мс`
);
}
}
async function jsonBodyMiddleware(req, res, next) {
const methodsWithBody = ["POST", "PUT", "PATCH"];
if (!methodsWithBody.includes(req.method)) {
await next();
return;
}
const contentType = req.headers["content-type"] || "";
if (!contentType.includes("application/json")) {
await next();
return;
}
const rawBody = await readRequestBody(req);
try {
req.body = rawBody ? JSON.parse(rawBody) : {};
} catch {
sendJson(res, 400, {
error: "Некоректний JSON"
});
return;
}
await next();
}
async function authMiddleware(req, res, next) {
const apiKey = req.headers["x-api-key"];
if (apiKey !== "dev-secret") {
sendJson(res, 401, {
error: "Потрібна автентифікація"
});
return;
}
req.user = {
id: 1,
name: "Demo user"
};
await next();
}
async function routeMiddleware(req, res, next) {
if (req.method === "GET" && req.url === "/profile") {
sendJson(res, 200, {
user: req.user
});
return;
}
if (req.method === "POST" && req.url === "/messages") {
sendJson(res, 201, {
message: "Повідомлення створено",
author: req.user,
data: req.body
});
return;
}
await next();
}
async function notFoundMiddleware(req, res) {
sendJson(res, 404, {
error: "Маршрут не знайдено"
});
}
const app = compose([
loggerMiddleware,
jsonBodyMiddleware,
authMiddleware,
routeMiddleware,
notFoundMiddleware
]);
const server = http.createServer((req, res) => {
app(req, res).catch((error) => {
console.error(error);
if (!res.headersSent) {
sendJson(res, 500, {
error: "Внутрішня помилка сервера"
});
}
});
});
server.listen(3000, () => {
console.log("Сервер запущено на http://localhost:3000");
});Збережіть код у файлі server.js і запустіть:
node server.jsЗапит до захищеного GET-маршруту:
curl -i \
-H "x-api-key: dev-secret" \
http://localhost:3000/profileЗапит із JSON-тілом:
curl -i \
-X POST \
-H "x-api-key: dev-secret" \
-H "Content-Type: application/json" \
-d '{"text":"Привіт"}' \
http://localhost:3000/messagesЯкщо не передати API-ключ, authMiddleware завершить запит зі статусом 401, а routeMiddleware не буде викликаний.
Порядок обробників у масиві має значення:
const app = compose([
loggerMiddleware,
jsonBodyMiddleware,
authMiddleware,
routeMiddleware,
notFoundMiddleware
]);У цьому прикладі:
loggerMiddleware отримує керування першим;
jsonBodyMiddleware додає req.body;
authMiddleware перевіряє доступ;
routeMiddleware створює основну відповідь;
notFoundMiddleware обробляє маршрути, яких не існує.
Якщо розташувати routeMiddleware перед jsonBodyMiddleware, обробник маршруту може отримати запит до того, як req.body буде заповнено.
Middleware має два основні варіанти поведінки.
async function exampleMiddleware(req, res, next) {
req.startTime = Date.now();
await next();
}async function exampleMiddleware(req, res, next) {
if (!req.headers["x-api-key"]) {
res.statusCode = 401;
res.end("Unauthorized");
return;
}
await next();
}Після res.end() не потрібно викликати next(). Відповідь уже завершена, тому подальші обробники не повинні її змінювати.
next() після завершення відповідіНеправильно:
function authMiddleware(req, res, next) {
if (!req.headers["x-api-key"]) {
res.statusCode = 401;
res.end("Unauthorized");
}
next();
}Якщо заголовка немає, сервер спочатку завершить відповідь, а потім спробує запустити наступний обробник.
Правильно:
function authMiddleware(req, res, next) {
if (!req.headers["x-api-key"]) {
res.statusCode = 401;
res.end("Unauthorized");
return;
}
next();
}await next()Якщо middleware асинхронний, зазвичай потрібно очікувати наступний обробник:
async function middleware(req, res, next) {
await next();
}Без await поточний middleware може завершитися раніше, ніж завершиться весь ланцюжок. Це особливо помітно в логуванні часу або під час обробки помилок.
next() більше одного разуMiddleware повинен передати керування далі лише один раз. Повторний виклик може спричинити повторне виконання обробників і помилку під час відправлення відповіді.
Якщо обробник використовує req.body або req.user, відповідний middleware має виконатися раніше.
Наприклад:
const app = compose([
jsonBodyMiddleware,
authMiddleware,
routeMiddleware
]);Тут routeMiddleware може використовувати і req.body, і req.user.
Помилки з await повинні доходити до центрального обробника:
app(req, res).catch((error) => {
console.error(error);
if (!res.headersSent) {
sendJson(res, 500, {
error: "Внутрішня помилка сервера"
});
}
});Без такого обробника відхилений Promise може залишитися необробленим.
Middleware — це проміжний обробник запиту та відповіді.
next() передає керування наступному middleware.
Відсутність next() дає змогу завершити запит достроково.
Middleware можна використовувати для логування, автентифікації, читання тіла запиту та інших повторюваних операцій.
Порядок middleware визначає порядок підготовки й обробки запиту.
Асинхронні middleware повинні коректно очікувати next() і передавати помилки до центрального обробника.
Після res.end() не слід викликати next().