Skip to main content

Що таке Express router і як його використовувати для модульного маршрутизації?

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.

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

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

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

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