Що таке middleware в Express.js і як він працює?
Middleware в Express.js - це функція, яка стоїть між вхідним HTTP-запитом і твоїм route handler-ом та має доступ до об'єктів req, res і колбека next.
Теорія
Коротко
- Уяви аеропортний контроль безпеки: кожен запит проходить через пункти перевірки по черзі, і кожен може перевірити, змінити або зупинити запит перед передачею далі.
- Основна механіка: виклик
next()продовжує ланцюг, відправка відповіді завершує його. - Використовуй для логіки, що стосується багатьох роутів (логування, авторизація, CORS). Для логіки одного хендлера - прямий handler простіший.
- Error middleware має чотири аргументи
(err, req, res, next)і реєструється після всього іншого middleware.
Швидкий приклад
const express = require('express');
const app = express();
// Middleware 1: логує час запиту
app.use((req, res, next) => {
console.log('Запит о:', new Date().toISOString());
next();
});
// Middleware 2: додає дані користувача до req
app.use((req, res, next) => {
req.user = { id: 123 };
next();
});
// Route handler бачить змінений req
app.get('/', (req, res) => {
res.json({ message: 'Привіт', userId: req.user.id });
// Вивід: { message: 'Привіт', userId: 123 }
});
app.listen(3000);Обидва middleware виконуються до route handler-а. Другий прикріплює req.user, тому handler може його прочитати, не знаючи звідки він узявся. Спільний об'єкт req і є суттю цього патерну.
Як Express будує ланцюг middleware
Кожен виклик app.use(fn) додає fn до внутрішнього масиву стека. Коли надходить запит, Express перебирає масив у порядку реєстрації і викликає кожну функцію. Функція сама вирішує що далі: next() продовжує, res.send() завершує цикл, next(err) переходить до обробника помилок.
Об'єкти req і res проходять через весь ланцюг незмінними. Будь-яка властивість, додана в middleware 1, доступна в middleware 5. Так middleware авторизації встановлює req.user, а всі наступні хендлери читають його без зайвих дій.
Коли використовувати
- Логування всіх запитів: реєструй один раз через
app.use()на початку файлу. - Перевірка JWT перед захищеними роутами: передавай middleware функцію безпосередньо роуту або групі роутів.
- Парсинг тіла запиту:
app.use(express.json())виконується до будь-якого handler-а. - CORS заголовки: middleware додає заголовки і викликає
next(), або одразу відповідає наOPTIONSзапити. - Перехоплення async помилок: error middleware з чотирма аргументами в кінці файлу.
Якщо логіка стосується лише одного роуту - прямий handler простіший і зрозуміліший.
Як стек працює зсередини
Node.js передає кожен HTTP-запит одному слухачу http.ServerRequest. Express підключається до нього і при кожному запиті синхронно обходить внутрішній стек через виклики next(). Блокуючий код зупиняє всі запити, бо Node - однопотоковий. Детально про це можна прочитати в статті про event loop.
Express 5 (release candidate станом на 2024 рік) автоматично перехоплює відхилені Promise з async middleware. У Express 4 треба самостійно обгортати в try/catch і викликати next(err). Більшість продакшн-кодових баз досі на Express 4, тому цей патерн актуальний і регулярно з'являється на співбесідах.
Типові помилки
Подвійний виклик next()
// Неправильно
app.use((req, res, next) => {
next();
next(); // викликає наступний middleware вдруге
});Другий виклик повторно запускає наступну функцію в стеку. Це призводить до дублювання логів, подвійних запитів до бази даних і помилки "headers already sent". Виклик next() - рівно один раз, або жодного, якщо відправляєш відповідь.
Реєстрація error middleware раніше часу
// Неправильно: logger не виконується при помилках
app.use(errorHandler);
app.use(logger);Express визначає error middleware за сигнатурою з чотирма аргументами. Якщо зареєструвати його раніше - воно перехоплює помилки до того, як решта ланцюга відпрацює. Має бути останнім.
Зміна req або res після відправки відповіді
app.use((req, res, next) => {
res.send('Готово');
req.foo = 'bar'; // ігнорується
next(); // не має ефекту
});Після res.send() відповідь зафіксована. Подальші зміни req або res губляться без будь-якої помилки. Якщо потік виконання неочевидний - перевіряй res.headersSent.
Блокуючий I/O в middleware
// Неправильно
app.use((req, res, next) => {
const data = fs.readFileSync('/large-file'); // зупиняє всі запити
next();
});Використовуй fs.promises.readFile з await. Синхронний I/O всередині middleware - одна з найпоширеніших причин деградації продуктивності в Express.
Відсутність try/catch в async middleware (Express 4)
// Неправильно в Express 4 - помилка обходить error handler
app.use(async (req, res, next) => {
await fetchUser(); // якщо відхиляється - ніхто не перехоплює
next();
});
// Правильно
app.use(async (req, res, next) => {
try {
await fetchUser();
next();
} catch (err) {
next(err); // спрямовує до error handler-а
}
});Необроблені async відхилення в Express 4 виводять попередження, але повністю обходять error handler. Якщо хочеш розібратись детальніше, як поширюються помилки в async функціях, - дивись статтю про async/await.
Де зустрічається у реальних проектах
express.json(): вбудований middleware, що парсить JSON тіло запиту до того як хендлери його побачать.helmet:app.use(helmet())встановлює набір заголовків безпеки одним рядком. Використовується в більшості продакшн Express-додатків.morgan:app.use(morgan('combined'))для структурованого логування HTTP запитів.passport.authenticate('jwt'): middleware авторизації, що перевіряє токени і заповнюєreq.user.- Mongoose pre/post хуки: той самий концептуальний патерн (ланцюг функцій) на рівні схеми, але поза Express.
Питання на співбесіді
Q: Який порядок виконання, якщо є і app.use(), і router-level middleware?
A: Спочатку app-level middleware в порядку реєстрації, потім router-level, потім route-specific хендлери. Всередині кожного рівня важливий порядок реєстрації.
Q: Як Express 4 обробляє async помилки на відміну від Express 5?
A: У Express 4 треба вручну try/catch і next(err). Express 5 автоматично перехоплює відхилені Promise з async middleware, тому обгортки не потрібні.
Q: Чи може middleware отримати доступ до req.params?
A: Залежить від реєстрації. Верхньорівневий app.use() без патерну шляху отримує req.params як порожній об'єкт. Route-specific і router middleware отримують параметри з визначеного шляху.
Q: Як пропустити решту middleware без відправки відповіді?
A: Виклич next('route') щоб перейти до наступного відповідного роут-хендлера. Тихо пропустити без відповіді або next('route') неможливо.
Q (senior): В кластеризованому Node.js, чи доживає стан з req до іншого воркера?
A: Ні. req існує лише в межах одного request-response циклу на одному воркер-процесі. Для даних, які мають зберігатись між запитами або воркерами, потрібне сховище сесій на базі Redis або бази даних. Стан middleware - завжди stateless, і розуміння цього відрізняє junior від senior відповіді.
Приклади
Логування і збагачення запиту
const express = require('express');
const app = express();
app.use((req, res, next) => {
req.startTime = Date.now();
console.log(`[${req.method}] ${req.url}`);
next();
});
app.use((req, res, next) => {
req.requestId = Math.random().toString(36).slice(2);
next();
});
app.get('/status', (req, res) => {
res.json({
requestId: req.requestId,
elapsed: Date.now() - req.startTime,
});
});
app.listen(3000);Два middleware додають дані до req. Хендлер читає обидва значення, не цікавлячись звідки вони. Таке розділення відповідальності тримає хендлери зосередженими на одній задачі і спрощує тестування кожного шару окремо.
Авторизація і CORS у продакшн стилі
const express = require('express');
const app = express();
// CORS middleware
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Authorization, Content-Type');
if (req.method === 'OPTIONS') return res.sendStatus(200);
next();
});
// JWT middleware авторизації
const authenticate = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Немає токена' });
// У продакшні: jwt.verify(token, process.env.SECRET, callback)
req.user = { id: 1, role: 'admin' }; // симуляція
next();
};
app.get('/protected', authenticate, (req, res) => {
res.json({ data: 'Секретний контент', user: req.user });
});
app.listen(3000);CORS middleware обробляє preflight-запити і завершує цикл там. Middleware авторизації передається лише тим роутам, яким він потрібен. return перед res.status(401) запобігає одночасному виклику next() після відправки відповіді.
Обробка async помилок через обгортку
const express = require('express');
const app = express();
// Хелпер для Express 4
const asyncHandler = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
app.use(
asyncHandler(async (req, res, next) => {
const user = await fetchUserFromDB(req.headers['x-user-id']);
if (!user) return res.status(404).json({ error: 'Не знайдено' });
req.user = user;
next();
})
);
app.get('/me', (req, res) => {
res.json({ user: req.user });
});
// Error handler - завжди останній
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'Внутрішня помилка' });
});
app.listen(3000);Обгортка asyncHandler перехоплює будь-який відхилений Promise і передає його до next, що спрямовує до error handler-а. Це стандартний патерн в кодових базах на Express 4. Express 5 прибирає потребу в обгортці.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.