Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке Express router і як його використовувати для модульного маршрутизації?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Express Router** - це окремий екземпляр маршрутизатора (`express.Router()`), який групує маршрути в окремих файлах і підключає їх до головного додатку за певним префіксом шляху. ```js const router = express.Router(); router.get('/', (req, res) => res.json({ users: [] })); // стає GET /api/users module.exports = router; // app.js app.use('/api/users', require('./routes/users')); ``` **Ключове:** шляхи всередині router відносні до точки монтування, а не до кореня сервера.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Express Router** - це окремий екземпляр маршрутизатора, створений через `express.Router()`. Він дозволяє визначати обробники маршрутів в окремих файлах і підключати їх до головного додатку за конкретним префіксом шляху. ## Теорія ### TL;DR - Router - це міні-додаток зі своїм стеком middleware, але без `.listen()`. Монтуй його, але не запускай самостійно. - Маршрути всередині router є **відносними** до точки монтування: `router.get('/profile')`, підключений до `/api/users`, стає `/api/users/profile`. - Менше 10 маршрутів у всьому проекті? Визначай прямо на `app`. З'явились кілька файлів або команд? Використовуй Router. - `app.use('/users', router)` - єдиний рядок, що з'єднує обидва. ### Швидкий приклад ```js // routes/users.js const express = require('express'); const router = express.Router(); router.get('/', (req, res) => res.json({ users: [] })); // GET /api/users router.get('/:id', (req, res) => res.json({ id: req.params.id })); // GET /api/users/123 module.exports = router; // app.js const userRouter = require('./routes/users'); app.use('/api/users', userRouter); // всі маршрути вище стають відносними до /api/users ``` `router.get('/')` не означає корінь сервера. Це означає "той шлях, куди мене змонтували". ### Чому шляхи відносні Без Router кожен маршрут живе в `app.js` з абсолютними шляхами. Один ресурс - це 5 маршрутів. Десять ресурсів - 50 маршрутів в одному файлі. Файл швидко добирається до 1000 рядків. Router створює відокремлену колекцію маршрутів. Коли ти викликаєш `app.use('/api/v1/users', userRouter)`, Express під час кожного запиту відрізає префікс і передає решту шляху до внутрішнього stack matcher цього router. Зміни точку монтування - всі маршрути всередині переїдуть разом. ### Коли використовувати - **Менше 10 маршрутів загалом** - визначай прямо на `app`. Зайві файли не потрібні. - **10+ маршрутів для одного ресурсу** - створи `routes/users.js`, підключи до `/users`. - **Команда розподілена по доменах** - один router на область (`auth`, `products`, `admin`), кожен у відповідального розробника. - **Версіонування API** - `v1Router` і `v2Router`, підключені до `/api/v1` і `/api/v2`. Не треба нічого переписувати при виході нової версії. - **Middleware для конкретного ресурсу** - помісти `requireAuth` всередину router, щоб захистити тільки цей ресурс. ### Middleware на рівні Router Middleware, підключений через `router.use()`, застосовується тільки до маршрутів у цьому router. Це відрізняється від `app.use()`, який спрацьовує на кожен вхідний запит. ```js // routes/admin.js const router = express.Router(); const { requireAdmin } = require('../middleware/auth'); router.use(requireAdmin); // захищає все нижче цього рядка router.get('/users', getAllUsers); router.delete('/users/:id', deleteUser); module.exports = router; ``` Одне правило щодо порядку: глобальний middleware на кшталт `express.json()` належить на `app`, а не всередині кожного router. Якщо помістити парсинг тіла тільки у `userRouter`, запити до `productRouter` не матимуть `req.body`. Я бачив як це ламало API в продакшені. ### Як Express обробляє монтування Коли викликаєш `express.Router()`, Express створює новий екземпляр `Router` із власним внутрішнім масивом `stack`. Кожен маршрут, який ти додаєш, записується як об'єкт `Layer` у цей stack разом із методом HTTP, патерном шляху та обробником. Під час запиту Express спочатку проходить по stack головного додатку. Коли зустрічає `app.use('/api/users', userRouter)`, відрізає префікс з URL і передає решту до stack matcher router. Якщо маршрут збігається - запускається обробник. Якщо ні - управління повертається до головного додатку. Ніякої магії. Просто вкладені стеки. ### Поширені помилки **Абсолютні шляхи всередині router** ```js // НЕПРАВИЛЬНО: при монтуванні до /users стає /users/users router.get('/users', (req, res) => res.json({ users: [] })); // ПРАВИЛЬНО router.get('/', (req, res) => res.json({ users: [] })); ``` Шляхи в router завжди відносні до точки монтування. Це найчастіша помилка на code review в Express на junior-рівні. **Забутий `module.exports`** ```js // users.js визначає все, але забуває цей рядок module.exports = router; // app.js app.use('/users', require('./users')); // TypeError: router is not a function ``` Повідомлення про помилку виглядає дивно, але виправлення завжди однакове: додай export. **Глобальний middleware після монтування** ```js // НЕПРАВИЛЬНО app.use('/users', userRouter); app.use(express.json()); // спрацьовує після монтування, вже запізно // ПРАВИЛЬНО app.use(express.json()); // завжди перед маршрутами app.use('/users', userRouter); ``` **Відсутність `{ mergeParams: true }` у вкладених router** Якщо вкладаєш router під параметризований шлях і потрібен доступ до батьківського параметра, дочірній router не побачить його за замовчуванням. ```js // без цього req.params.userId буде undefined const orderRouter = express.Router({ mergeParams: true }); orderRouter.get('/', (req, res) => { res.json({ userId: req.params.userId, orders: [] }); // працює лише з mergeParams }); router.use('/:userId/orders', orderRouter); ``` Ця помилка часто трапляється у розробників середнього рівня. Якщо `req.params` виглядає порожнім у дочірньому router - спочатку перевір цю опцію. ### Де зустрічається в реальних проектах - **MERN-стек** (наприклад, fullstackopen.com) - `routes/api/users.js` підключений до `/api` для fetch-запитів з фронтенду. - **NestJS** - використовує Express Router під капотом: кожен `@Controller('users')` компілюється в екземпляр Router. - **FeathersJS** - кожен сервіс монтується як Router за шляхом ресурсу. - **Express boilerplate** (наприклад, hagopj13/node-express-boilerplate на GitHub) - один router на домен, підключається в `app.js`. ### Follow-up питання **Q:** Яка різниця між об'єктами `app` і `router`? **A:** Обидва мають `.use()`, `.get()`, `.post()` та інші методи. Головна відмінність: у `router` немає `.listen()`. Не можна запустити сервер з router. Він працює тільки після монтування в app. **Q:** Як відрізняється область дії `router.use()` від `app.use()`? **A:** `router.use(fn)` застосовується тільки до маршрутів конкретного router. `app.use(fn)` запускається на кожен вхідний запит перед будь-яким router. **Q:** Що таке `router.param()` і коли його використовувати? **A:** `router.param('id', fn)` запускає callback щоразу, коли `:id` з'являється в збіжному маршруті, до запуску обробника. Корисно для завантаження юзера з бази один раз і прикріплення до `req.user`, замість повторення цієї логіки в кожному обробнику. **Q:** Чи може router мати власний обробник помилок? **A:** Так. Додай middleware з чотирма аргументами в кінці: `router.use((err, req, res, next) => { ... })`. Він перехоплює помилки з цього router. Якщо викликає `next(err)`, помилка піднімається до обробника рівня app. **Q:** Як версіонувати API через router? **A:** Створи `v1Router` і `v2Router`, підключи до `/api/v1` і `/api/v2`. Старі клієнти продовжують звертатись до v1, нові використовують v2. Існуючі обробники не змінюються. ## Приклади ### Базовий: один ресурс у окремому файлі ```js // routes/products.js const express = require('express'); const router = express.Router(); router.get('/', (req, res) => res.json({ products: ['laptop', 'phone'] })); // GET /products router.post('/', (req, res) => res.status(201).json({ id: 1 })); // POST /products router.get('/:id', (req, res) => res.json({ id: req.params.id })); // GET /products/42 module.exports = router; // app.js const express = require('express'); const app = express(); app.use(express.json()); app.use('/products', require('./routes/products')); app.listen(3000); ``` Три маршрути, один файл. `app.js` залишається чистим незалежно від кількості обробників всередині `products.js`. ### Середній: захищений API користувачів із middleware Це ближче до того, що побачиш у реальному MERN-проекті. ```js // middleware/auth.js const requireAuth = (req, res, next) => { const token = req.headers.authorization; if (!token) return res.status(401).json({ error: 'Unauthorized' }); // тут була б перевірка JWT next(); }; module.exports = { requireAuth }; // routes/users.js const express = require('express'); const { requireAuth } = require('../middleware/auth'); const router = express.Router(); router.use(requireAuth); // всі маршрути нижче потребують авторизації router.get('/', async (req, res) => { res.json({ users: [{ id: 1, name: 'Alice' }] }); // GET /api/users (захищений) }); router.post('/', async (req, res) => { const { name } = req.body; res.status(201).json({ id: 2, name }); // POST /api/users (захищений) }); module.exports = router; // app.js app.use(express.json()); app.use('/api/users', require('./routes/users')); ``` Перевірка авторизації запускається один раз для всього router. Не треба повторювати її в кожному обробнику. ### Просунутий: вкладені router з `mergeParams` Вкладені ресурси на кшталт `/users/123/orders` потребують уважного налаштування. ```js // routes/orders.js const express = require('express'); const orderRouter = express.Router({ mergeParams: true }); // успадкувати :userId від батька orderRouter.get('/', (req, res) => { // req.params.userId доступний тут завдяки mergeParams res.json({ userId: req.params.userId, orders: [] }); }); module.exports = orderRouter; // routes/users.js const express = require('express'); const router = express.Router(); const orderRouter = require('./orders'); router.param('userId', (req, res, next, id) => { req.user = { id, name: 'Alice' }; // завантажити юзера до запуску будь-якого обробника next(); }); router.use('/:userId/orders', orderRouter); // GET /users/123/orders module.exports = router; ``` Без `{ mergeParams: true }` значення `req.params.userId` у `orderRouter` буде `undefined`. Це поширений баг у багаторівневих REST API.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.