Skip to main content

Що таке middleware в Express.js і як він працює?

Middleware в Express.js - це функція, яка стоїть між вхідним HTTP-запитом і твоїм route handler-ом та має доступ до об'єктів req, res і колбека next.

Теорія

Коротко

  • Уяви аеропортний контроль безпеки: кожен запит проходить через пункти перевірки по черзі, і кожен може перевірити, змінити або зупинити запит перед передачею далі.
  • Основна механіка: виклик next() продовжує ланцюг, відправка відповіді завершує його.
  • Використовуй для логіки, що стосується багатьох роутів (логування, авторизація, CORS). Для логіки одного хендлера - прямий handler простіший.
  • Error middleware має чотири аргументи (err, req, res, next) і реєструється після всього іншого middleware.

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

javascript
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()

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

Другий виклик повторно запускає наступну функцію в стеку. Це призводить до дублювання логів, подвійних запитів до бази даних і помилки "headers already sent". Виклик next() - рівно один раз, або жодного, якщо відправляєш відповідь.

Реєстрація error middleware раніше часу

javascript
// Неправильно: logger не виконується при помилках app.use(errorHandler); app.use(logger);

Express визначає error middleware за сигнатурою з чотирма аргументами. Якщо зареєструвати його раніше - воно перехоплює помилки до того, як решта ланцюга відпрацює. Має бути останнім.

Зміна req або res після відправки відповіді

javascript
app.use((req, res, next) => { res.send('Готово'); req.foo = 'bar'; // ігнорується next(); // не має ефекту });

Після res.send() відповідь зафіксована. Подальші зміни req або res губляться без будь-якої помилки. Якщо потік виконання неочевидний - перевіряй res.headersSent.

Блокуючий I/O в middleware

javascript
// Неправильно 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)

javascript
// Неправильно в 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 відповіді.

Приклади

Логування і збагачення запиту

javascript
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 у продакшн стилі

javascript
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 помилок через обгортку

javascript
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 прибирає потребу в обгортці.

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

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

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

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