Які існують різні типи проміжного програмного забезпечення в Express.js?
Middleware в Express.js - функції, що виконуються між отриманням HTTP-запиту і відправкою відповіді, кожна з доступом до req, res і next().
Теорія
TL;DR
- Express має 5 типів middleware: application-level, router-level, обробка помилок, вбудований, сторонній
- Уявляй це як конвеєр: кожна станція опрацьовує запит і передає його далі через
next() - Application-level виконується глобально, router-level обмежений шляхом монтування
- Обробник помилок використовує 4-параметровий підпис
(err, req, res, next)і монтується останнім - Немає виклику
next()у middleware - запит зависає назавжди
Швидкий приклад
const express = require('express');
const app = express();
// Application-level: виконується для всіх запитів
app.use((req, res, next) => {
console.log(`${req.method} ${req.path}`); // GET /api/users
next();
});
// Router-level: обмежений /api
const router = express.Router();
router.use((req, res, next) => {
console.log('Router hit'); // тільки для /api/*
next();
});
app.use('/api', router);Для GET /api/users спрацюють обидва логи по черзі. Для GET / тільки перший. Ця різниця і пояснює весь поділ між app і router.
Типи middleware
1. Application-level (на рівні застосунку)
Прив'язується до об'єкта app через app.use() або app.METHOD(). Виконується глобально або для конкретного шляху.
app.use(express.json()); // для всіх маршрутів
app.get('/users', (req, res, next) => { next(); }); // тільки GET /users2. Router-level (на рівні роутера)
Прив'язується до express.Router(). Поведінка така ж як у application-level, але обмежена шляхом монтування. Стандартний спосіб розбити великий API на модулі.
const userRouter = express.Router();
userRouter.use(authCheck); // виконується тільки для /api/users/*
app.use('/api/users', userRouter);3. Error-handling (обробка помилок)
Express розпізнає цей тип за 4-параметровим підписом (err, req, res, next). Переходить до нього, коли будь-який інший middleware викликає next(err). Завжди монтується останнім.
app.use((err, req, res, next) => {
res.status(err.status || 500).json({ error: err.message });
});4. Built-in (вбудований)
Входить до складу Express починаючи з версії 4.16. Три функції закривають найпоширеніші потреби:
app.use(express.json()); // парсить JSON-тіла
app.use(express.urlencoded({ extended: true })); // парсить форми
app.use(express.static('public')); // роздає статичні файли5. Third-party (сторонній)
npm-пакети, що підключаються до тієї ж системи middleware:
app.use(require('helmet')()); // заголовки безпеки
app.use(require('cors')()); // CORS-заголовки
app.use(require('morgan')('dev')); // логування запитівПорядок виконання
Express тримає стек функцій middleware для кожного застосунку або роутера, виконуючи їх у порядку монтування. При отриманні запиту Express проходить стек зверху вниз. Кожна функція або викликає next() щоб продовжити, або відправляє відповідь щоб зупинити ланцюжок, або викликає next(err) щоб перейти до обробника помилок.
app.use(cors()); // 1
app.use(helmet()); // 2
app.use(express.json()); // 3
app.use('/api', routes); // 4 - обробники маршрутів
app.use(notFound); // 5 - 404
app.use(errorHandler); // 6 - завжди останнєСтек роутера це підстек застосунку, підключений до конкретного шляху. Тому userRouter.use(authCheck) разом з app.use('/api', userRouter) означає, що authCheck запускається тільки коли шлях починається з /api.
Типові помилки
Забутий next() в async middleware
// Неправильно - запит зависає назавжди
app.use(async (req, res, next) => {
await db.connect();
// next() не викликається
});
// Правильно
app.use(async (req, res, next) => {
try {
await db.connect();
next();
} catch (err) {
next(err);
}
});Обробник помилок змонтований занадто рано
// Неправильно - обробник помилок до маршрутів
app.use((err, req, res, next) => { res.status(500).send('Error'); });
app.get('/', (req, res) => res.send('Hi'));
// Правильно
app.get('/', (req, res) => res.send('Hi'));
app.use((err, req, res, next) => { res.status(500).send('Error'); });express.json() підключений після роутера
Проблема з порядком підключення парсера тіла трапляється з більшістю розробників у першому реальному Express-проекті. Це перше що треба перевіряти коли req.body показує undefined.
// Неправильно - req.body буде undefined в userRouter
app.use('/api', userRouter);
app.use(express.json()); // запізно
// Правильно
app.use(express.json()); // спочатку парсер
app.use('/api', userRouter);Необроблені async-помилки в Express 4
Async-функція яка кидає виключення не викликає next(err) автоматично в Express 4. Помилка повністю обходить обробник.
// Неправильно в Express 4 - помилка не потрапить до обробника
app.get('/data', async (req, res, next) => {
throw new Error('Boom');
});
// Правильно - явний try/catch
app.get('/data', async (req, res, next) => {
try {
const data = await fetchData();
res.json(data);
} catch (err) {
next(err);
}
});Express 5 (наразі в beta) обробляє async-виключення автоматично.
Конфліктні шляхи роутерів перекривають один одного
app.use('/users', userRouter); // спрацьовує
app.use('/users/:id', idRouter); // ніколи не спрацює, перекритийВиправлення: реєструй роутери від специфічних до загальних, або об'єднай логіку в один роутер.
Де зустрічається в реальних проектах
morganдля логування майже в кожному Express-застосунку:app.use(morgan('combined'))helmetяк базовий мінімум безпеки:app.use(helmet())- Router-level авторизація для групи маршрутів
/api/v1в REST API - NestJS використовує application-level middleware для guards та interceptors під капотом
- NextAuth.js обгортає свою групу маршрутів
/api/authвexpress.Router()
Питання на співбесіді
Q: Який порядок виконання для вкладених роутерів?
A: Спочатку application-level middleware, потім middleware зовнішнього роутера, потім внутрішнього, потім обробник маршруту. next() з'єднує їх лінійно зверху вниз.
Q: Яка різниця між app.METHOD() і app.use()?
A: app.get() відповідає лише GET-запитам і точному шляху. app.use() відповідає всім HTTP-методам і будь-якому шляху з відповідним префіксом. Тому глобальний middleware завжди використовує app.use().
Q: Що буде якщо middleware викличе next() двічі?
A: Другий виклик ігнорується. Помилки немає, але поведінка непередбачувана якщо відповідь вже відправлена.
Q: Яка різниця між express.json() і body-parser?
A: На практиці різниці немає. body-parser перенесли до ядра Express у версії 4.16. express.json() викликає той самий код.
Q (senior): В мікросервісі з 50+ функцій middleware як уникнути проблем з продуктивністю без втрати порядку виконання?
A: Групуй пов'язані функції в масиви: const apiPipeline = [cors(), helmet(), express.json()], потім app.use('/api', apiPipeline, routes). Профілюй за допомогою clinic.js або 0x. Уникай синхронних блокуючих операцій в middleware.
Приклади
Базовий: глобальне логування і парсинг JSON
const express = require('express');
const app = express();
// Вбудований: парсить JSON-тіла до того як маршрути читають req.body
app.use(express.json());
// Application-level: логування кожного запиту
app.use((req, res, next) => {
console.log(`${req.method} ${req.path}`); // GET /users
next();
});
app.get('/users', (req, res) => {
res.json([{ id: 1, name: 'Alice' }]);
});
app.listen(3000);Кожен запит спочатку потрапляє до логера, потім до обробника маршруту. Якщо поміняти ці два app.use() місцями, логер все одно працює, але POST-маршрут що читає req.body отримає undefined.
Середній: авторизація на рівні роутера
const express = require('express');
const app = express();
const userRouter = express.Router();
app.use(express.json());
// Router-level: перевірка токена тільки для /api/users
userRouter.use((req, res, next) => {
if (req.headers.authorization !== 'Bearer secret') {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
});
userRouter.post('/', (req, res) => {
res.status(201).json({ id: 1, name: req.body.name }); // { id: 1, name: 'Alice' }
});
app.use('/api/users', userRouter);
app.listen(3000);
// POST /api/users без токена -> 401
// POST /api/users з Bearer secret -> 201Перевірка авторизації виконується тільки для /api/users/*. Публічний маршрут GET /health її не торкається. Саме в цьому практична перевага router-level middleware над глобальним.
Для досвідчених: async-обробка помилок через весь ланцюжок
const express = require('express');
const app = express();
app.use(express.json());
const findUser = (id) => {
if (id !== '1') {
const err = new Error('User not found');
err.status = 404;
throw err;
}
return { id: 1, name: 'Alice' };
};
app.get('/users/:id', async (req, res, next) => {
try {
const user = findUser(req.params.id);
res.json(user);
} catch (err) {
next(err); // переходить до обробника помилок нижче
}
});
// 4 параметри, монтується останнім
app.use((err, req, res, next) => {
console.error(err.stack); // Error: User not found\n at ...
res.status(err.status || 500).json({ error: err.message });
});
app.listen(3000);
// GET /users/1 -> { id: 1, name: 'Alice' }
// GET /users/99 -> 404 { error: 'User not found' }Якщо прибрати try/catch і кидати виключення напряму з async-обробника в Express 4, помилка обходить next(err) повністю. Явне загортання async-коду обов'язкове поки Express 5 не стане стабільним.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.