Що таке проміжне програмне забезпечення в Express.js і як воно працює?
Middleware в Express.js - це функція, яка стоїть між вхідним HTTP-запитом і обробником маршруту, має доступ до req, res і функції next() для передачі управління далі.
Теорія
TL;DR
- Черга на паспортний контроль: кожен пункт перевірки (middleware) перевіряє документи (
req), ставить штамп (res), пропускає далі (next()). Будь-який пункт може зупинити. - Middleware виконується в тому порядку, в якому ти його реєструєш. Порядок важливий.
- Не викликав
next()- запит зависає. Або відправ відповідь, або передай управління. - Error middleware приймає 4 аргументи:
(err, req, res, next). Реєструй його останнім. - Shared логіка для багатьох маршрутів (логування, auth) - у middleware. Бізнес-логіка - в обробнику маршруту.
Швидкий приклад
const express = require('express');
const app = express();
// Логування - виконується на кожному запиті
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`); // GET /users
next(); // передаємо управління далі
});
// Auth - тільки для /users
app.use('/users', (req, res, next) => {
if (req.headers.authorization) {
next(); // токен є, продовжуємо
} else {
res.status(401).send('Unauthorized'); // зупиняємось тут
}
});
// Обробник маршруту - досягається тільки якщо auth пройшов
app.get('/users', (req, res) => {
res.json({ users: ['Alice', 'Bob'] });
});
app.listen(3000);GET /users з токеном: запит логується, auth проходить, повертаємо користувачів. Без токена: 401 і все.
Як це працює всередині
Express будує стек middleware-функцій під час налаштування додатку. Їх зберігають як шари в app._router.stack. Коли приходить запит, Node викликає app.handle(req, res), який проходить по стеку один шар за одним. Кожен шар виконує свою функцію і передає next як диспетчер для переходу до наступного відповідного шару або маршруту.
Async middleware не викликає next() автоматично. Це треба робити вручну, інакше запит зависає. Більшість розробників стикаються з цим при першому знайомстві з async middleware в Express.
Типи middleware
Application-level middleware реєструється через app.use() і виконується на кожному запиті (або кожному запиті з певним префіксом шляху).
Route-level middleware прив'язується до конкретного маршруту: app.get('/path', middleware, handler). Можна вказати кілька функцій перед кінцевим обробником.
Error-handling middleware має рівно 4 параметри: (err, req, res, next). Express розпізнає цей підпис і автоматично направляє туди помилки при виклику next(err).
Вбудоване: express.json(), express.urlencoded(), express.static() закривають більшість потреб без додаткових пакетів.
Стороннє: morgan для логування, helmet для заголовків безпеки, cors для cross-origin запитів, express-rate-limit для обмеження кількості запитів.
Коли використовувати
- Спільна логіка для всіх маршрутів (логування, CORS, заголовки безпеки):
app.use()на початку файлу. - Перевірки для конкретного маршруту (auth, валідація): передай middleware безпосередньо у визначення маршруту.
- Обробка помилок:
app.use((err, req, res, next) => {...})після всіх маршрутів. - Статичні файли:
express.static('public'). - Бізнес-логіка належить обробнику маршруту, не middleware.
next(), next('route') і next(err)
next() // перейти до наступного middleware або обробника маршруту
next('route') // пропустити решту middleware в стеку, перейти до наступного маршруту
next(err) // перейти прямо до обробника помилокnext('route') - деталь рівня senior. Дозволяє писати fallback маршрути: виконав middleware, вирішив що цей маршрут не підходить, і передав запит до наступного відповідного визначення маршруту.
Типові помилки
Забутий next() в async middleware
// Неправильно - запит зависає назавжди
app.use(async (req, res, next) => {
req.data = await db.query('SELECT * FROM users');
// next() не викликається
});
// Правильно
app.use(async (req, res, next) => {
try {
req.data = await db.query('SELECT * FROM users');
next();
} catch (err) {
next(err);
}
});Express виконує middleware синхронно. Async функція одразу повертає Promise, але Express не чекає на нього. await завершується, а next() так і не викликається.
Глобальне middleware зареєстроване після маршрутів
// Неправильно - logger ніколи не спрацює для /users
app.get('/users', handler);
app.use(logger);
// Правильно
app.use(logger);
app.get('/users', handler);Маршрути матчаться першими. app.use(), зареєстрований після маршруту, не виконується для цього маршруту.
Подвійний виклик next()
// Неправильно - запускає наступне middleware двічі
app.use((req, res, next) => {
next();
next(); // другий виклик ламає стек
});Дублює логування, обробку і часто дає помилку "headers already sent". Виклик next() - рівно один раз на middleware.
Error handler перед маршрутами
// Неправильно
app.use(errorHandler);
app.get('/users', handler);
// Правильно - error handler завжди останній
app.get('/users', handler);
app.use(errorHandler);Неправильний порядок залежностей у req
Якщо твоє middleware читає req.body, переконайся що express.json() зареєстрований раніше. Інакше req.body буде undefined і middleware мовчки не спрацює.
Де зустрічається в реальних проектах
morgan: логування HTTP-запитів у production APIhelmet: заголовки безпеки (більше 1 мільйона npm-проектів)passport.js: автентифікація черезapp.use(passport.initialize())cors: cross-origin запити для зв'язки frontend і backendexpress-rate-limit: захист/apiмаршрутів від зловживаньexpress.json(): замінив старий пакетbody-parser(вбудовано починаючи з Express 4.16)
Питання на інтерв'ю
Q: Яка різниця між app.use() і router.use()?
A: app.use() реєструє middleware глобально на рівні додатку. router.use() обмежує його до sub-router, тобто middleware виконується тільки для маршрутів цього роутера. Зручно при розбивці великого API на окремі модулі.
Q: Чи можна використовувати async/await у middleware? Які підводні камені?
A: Так. Але next() треба викликати вручну після завершення async-операції, а весь код обгорнути в try/catch для виклику next(err) при помилці. Необроблені відхилення (unhandled rejections) крашають Node-процес у версіях 15+ і мовчки дропають запит у старіших.
Q: Як працює next('route')?
A: Пропускає всі middleware, що залишились у поточному стеку, і переходить до наступного відповідного маршруту. Зручно для умовної маршрутизації: перевірив прапорець і передав запит на fallback обробник.
Q: Що станеться якщо викликати res.send() і потім next()?
A: Відповідь вже надіслано. next() просуне стек далі, але будь-яке middleware що спробує записати в res отримає помилку "headers already sent". Додай return перед res.send(), щоб уникнути цього.
Q: Як впливає великий стек middleware на продуктивність?
A: Мінімально. Синхронний прохід по стеку займає приблизно 1 мікросекунду на шар. Справжнє вузьке місце - async I/O, не глибина стека. Для вимірювання використовуй clinic.js.
Приклади
Базовий: логування запитів і auth-перевірка
const express = require('express');
const app = express();
app.use(express.json());
// Логер - фіксує метод, URL і позначає час старту
app.use((req, res, next) => {
req.startTime = Date.now();
console.log(`--> ${req.method} ${req.url}`);
next();
});
// Auth для захищених маршрутів
function requireAuth(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Token required' });
req.user = { id: 42 }; // в реальному коді: верифікація JWT
next();
}
app.get('/public', (req, res) => {
res.json({ message: 'Відкрито для всіх' });
});
app.get('/private', requireAuth, (req, res) => {
res.json({ message: 'Hello', userId: req.user.id });
});
app.listen(3000);Логер виконується для обох маршрутів. requireAuth - тільки для /private. Якщо токен відсутній, обробник маршруту не виконується.
Середній: async middleware з правильною обробкою помилок
const fakeDB = {
findUser: async (token) => {
if (token === 'valid') return { id: 1, name: 'Alice', admin: true };
throw new Error('User not found');
}
};
app.get('/profile',
// Крок 1: завантажуємо користувача з DB по токену
async (req, res, next) => {
try {
req.user = await fakeDB.findUser(req.query.token);
next();
} catch (err) {
next(err); // передаємо до error handler, не res.send()
}
},
// Крок 2: перевіряємо права адміна
(req, res, next) => {
if (!req.user.admin) {
return next(new Error('Access denied'));
}
next();
},
// Крок 3: відповідаємо
(req, res) => {
res.json(req.user);
}
);
// Центральний обробник помилок - останній, 4 аргументи
app.use((err, req, res, next) => {
console.error(err.message);
res.status(500).json({ error: err.message });
});Без next(err) в async middleware - необроблене відхилення Promise крашає процес (Node 15+) або мовчки дропає запит у старіших версіях. try/catch з next(err) - правильний патерн.
Senior: rate limiter з контекстом запиту
const express = require('express');
const app = express();
// Додаємо час старту та ID користувача до кожного запиту
app.use((req, res, next) => {
req.startTime = Date.now();
req.userId = req.headers['x-user-id'] || 'anonymous';
next();
});
// Простий in-memory rate limiter для /api маршрутів
const requestCounts = {};
app.use('/api', (req, res, next) => {
const key = req.userId;
requestCounts[key] = (requestCounts[key] || 0) + 1;
if (requestCounts[key] > 100) {
return res.status(429).json({ error: 'Rate limit exceeded' });
}
next();
});
app.get('/api/users/:id', (req, res) => {
const elapsed = Date.now() - req.startTime;
console.log(`${req.userId} запросив користувача ${req.params.id} за ${elapsed}ms`);
res.json({ id: req.params.id, name: 'Jane' });
});
app.listen(3000);Перше middleware збагачує req контекстом, який вільно читається далі по стеку. Поширений патерн у production API: одне middleware заповнює req.user, req.requestId, req.startTime, а решта стека використовує ці значення без повторних запитів до бази.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.