Skip to main content

Що таке проміжне програмне забезпечення в 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. Бізнес-логіка - в обробнику маршруту.

Швидкий приклад

js
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)

js
next() // перейти до наступного middleware або обробника маршруту next('route') // пропустити решту middleware в стеку, перейти до наступного маршруту next(err) // перейти прямо до обробника помилок

next('route') - деталь рівня senior. Дозволяє писати fallback маршрути: виконав middleware, вирішив що цей маршрут не підходить, і передав запит до наступного відповідного визначення маршруту.

Типові помилки

Забутий next() в async middleware

js
// Неправильно - запит зависає назавжди 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 зареєстроване після маршрутів

js
// Неправильно - logger ніколи не спрацює для /users app.get('/users', handler); app.use(logger); // Правильно app.use(logger); app.get('/users', handler);

Маршрути матчаться першими. app.use(), зареєстрований після маршруту, не виконується для цього маршруту.

Подвійний виклик next()

js
// Неправильно - запускає наступне middleware двічі app.use((req, res, next) => { next(); next(); // другий виклик ламає стек });

Дублює логування, обробку і часто дає помилку "headers already sent". Виклик next() - рівно один раз на middleware.

Error handler перед маршрутами

js
// Неправильно 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 API
  • helmet: заголовки безпеки (більше 1 мільйона npm-проектів)
  • passport.js: автентифікація через app.use(passport.initialize())
  • cors: cross-origin запити для зв'язки frontend і backend
  • express-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-перевірка

js
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 з правильною обробкою помилок

js
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 з контекстом запиту

js
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, а решта стека використовує ці значення без повторних запитів до бази.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?