Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке middleware в Express.js і як він працює?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Middleware** в Express.js - це функція з доступом до `req`, `res` і `next`, яка виконується під час обробки HTTP-запиту. Express запускає middleware в порядку реєстрації: `next()` продовжує ланцюг, відправка відповіді завершує цикл. Будь-яка властивість, додана до `req` в одному middleware, доступна у всіх наступних хендлерах. **Головне:** error middleware потребує чотири аргументи і реєструється останнім.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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](/questions/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](/questions/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 прибирає потребу в обгортці.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.