Роутинг в Express

Зачем нужно

Роутинг определяет как приложение отвечает на запрос клиента к конкретному URL и HTTP-методу. Вместо одного огромного if/else для всех путей Express дает декларативный API: app.get('/users', handler), app.post('/users', handler). Правильная организация маршрутов -- ключ к поддерживаемому API.

Где используется

  • REST API: маршруты для каждого ресурса (users, posts, comments)
  • Веб-приложения: страницы (/, /about, /login)
  • Микросервисы: группы маршрутов для доменов
  • API-версионирование: /api/v1/users, /api/v2/users

Предпосылки


Базовый роутинг

const express = require('express');
const app = express;

// app.METHOD(PATH, HANDLER)

// GET — получить ресурс
app.get('/', (req, res) => {
  res.send('Home page');
});

// POST — создать ресурс
app.post('/api/users', (req, res) => {
  res.status(201).json({ id: 1, name: req.body.name });
});

// PUT — полная замена ресурса
app.put('/api/users/:id', (req, res) => {
  res.json({ id: req.params.id, ...req.body });
});

// PATCH — частичное обновление ресурса
app.patch('/api/users/:id', (req, res) => {
  res.json({ id: req.params.id, updated: true });
});

// DELETE — удалить ресурс
app.delete('/api/users/:id', (req, res) => {
  res.status(204).end();
});

// ALL — любой HTTP-метод
app.all('/api/secret', (req, res) => {
  res.status(403).json({ error: 'Access denied' });
});

app.listen(3000);

Параметры маршрута — req.params

// :param — именованный параметр в URL

// /api/users/42
app.get('/api/users/:id', (req, res) => {
  console.log(req.params);     // { id: '42' }
  console.log(req.params.id);  // '42' (всегда строка!)

  const id = Number(req.params.id);
  res.json({ id });
});

// Несколько параметров: /api/users/42/posts/7
app.get('/api/users/:userId/posts/:postId', (req, res) => {
  console.log(req.params);
  // { userId: '42', postId: '7' }
  res.json(req.params);
});

// Опциональный параметр: /api/users или /api/users/42
app.get('/api/users/:id?', (req, res) => {
  if (req.params.id) {
    res.json({ user: req.params.id });
  } else {
    res.json({ users: 'all' });
  }
});

Query-параметры — req.query

// Query string: ?key=value&key2=value2
// Автоматически парсится Express

// GET /api/users?page=2&limit=10&sort=name
app.get('/api/users', (req, res) => {
  console.log(req.query);
  // { page: '2', limit: '10', sort: 'name' }

  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 20;
  const sort = req.query.sort() || 'id';

  // Пагинация и сортировка
  res.json({ page, limit, sort, data:  });
});

// GET /api/search?q=nodejs&tags=backend&tags=javascript
app.get('/api/search', (req, res) => {
  console.log(req.query.q);    // 'nodejs'
  console.log(req.query.tags); // ['backend', 'javascript'] (массив)
  res.json({ query: req.query });
});

Тело запроса — req.body

const express = require('express');
const app = express;

// Обязательно подключить middleware для парсинга тела!
app.use(express.json());                          // JSON
app.use(express.urlencoded({ extended: true }));  // form data

// POST /api/users
// Body: { "name": "Anna", "email": "anna@example.com" }
app.post('/api/users', (req, res) => {
  console.log(req.body);
  // { name: 'Anna', email: 'anna@example.com' }

  const { name, email } = req.body;

  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email are required' });
  }

  res.status(201).json({ id: Date.now(), name, email });
});

express.Router — модульный роутинг

Router создает изолированную группу маршрутов, которую можно подключить к приложению с префиксом.

// routes/users.js
const express = require('express');
const router = express.Router;

// Все маршруты относительно точки подключения
// (будет /api/users/...)

// GET /api/users
router.get('/', (req, res) => {
  res.json([
    { id: 1, name: 'Anna' },
    { id: 2, name: 'Boris' }
  ]);
});

// GET /api/users/:id
router.get('/:id', (req, res) => {
  res.json({ id: req.params.id, name: 'User' });
});

// POST /api/users
router.post('/', (req, res) => {
  res.status(201).json({ id: Date.now(), ...req.body });
});

// PUT /api/users/:id
router.put('/:id', (req, res) => {
  res.json({ id: req.params.id, ...req.body });
});

// DELETE /api/users/:id
router.delete('/:id', (req, res) => {
  res.status(204).end();
});

module.exports = router;
// app.js
const express = require('express');
const app = express;

const userRoutes = require('./routes/users');
const postRoutes = require('./routes/posts');
const authRoutes = require('./routes/auth');

app.use(express.json());

// Подключение роутеров с префиксами
app.use('/api/users', userRoutes);   // /api/users/*
app.use('/api/posts', postRoutes);   // /api/posts/*
app.use('/api/auth', authRoutes);    // /api/auth/*

app.listen(3000);

Вложенные роутеры

// routes/users.js
const express = require('express');
const router = express.Router;
const postRouter = require('./userPosts');

// Вложенный роутер: /api/users/:userId/posts
router.use('/:userId/posts', postRouter);

router.get('/', (req, res) => { /* ... */ });
router.get('/:id', (req, res) => { /* ... */ });

module.exports = router;

// routes/userPosts.js
const express = require('express');
const router = express.Router({ mergeParams: true }); // ← mergeParams!

// GET /api/users/:userId/posts
router.get('/', (req, res) => {
  const { userId } = req.params; // доступен благодаря mergeParams
  res.json({ userId, posts:  });
});

module.exports = router;

Порядок маршрутов имеет значение

Express проверяет маршруты в порядке определения. Первый совпавший обрабатывает запрос.

// ❌ НЕПРАВИЛЬНЫЙ порядок
app.get('/api/users/:id', (req, res) => {
  res.json({ id: req.params.id });
});

app.get('/api/users/me', (req, res) => {
  // Этот маршрут НИКОГДА не вызовется!
  // '/api/users/me' совпадёт с ':id' выше (id = 'me')
  res.json({ user: 'current' });
});

// ✅ ПРАВИЛЬНЫЙ порядок: конкретные маршруты ДО параметризованных
app.get('/api/users/me', (req, res) => {
  res.json({ user: 'current' });
});

app.get('/api/users/:id', (req, res) => {
  res.json({ id: req.params.id });
});

Regex-маршруты

// Строковые паттерны (Express route syntax)
app.get('/ab?cd', handler);       // /acd или /abcd
app.get('/ab+cd', handler);       // /abcd, /abbcd, /abbbcd ...
app.get('/ab*cd', handler);       // /abcd, /ab123cd, /abXYZcd ...

// Полноценный RegExp
app.get(/.*fly$/, (req, res) => {
  // Любой URL, оканчивающийся на 'fly'
  // /butterfly, /dragonfly
  res.send(`Matched: ${req.url}`);
});

// Параметры с ограничением формата
app.get('/api/users/:id(\\d+)', (req, res) => {
  // Только числовые ID: /api/users/42 ✅
  // /api/users/abc ❌ (не совпадёт)
  res.json({ id: Number(req.params.id) });
});

route — цепочка методов

// Вместо повторения пути:
app.get('/api/books', getBooks);
app.post('/api/books', createBook);
app.put('/api/books', updateBook);

// Используй app.route:
app.route('/api/books')
  .get((req, res) => {
    res.json([{ id: 1, title: 'Node.js in Action' }]);
  })
  .post((req, res) => {
    res.status(201).json(req.body);
  });

app.route('/api/books/:id')
  .get((req, res) => {
    res.json({ id: req.params.id });
  })
  .put((req, res) => {
    res.json({ id: req.params.id, ...req.body });
  })
  .delete((req, res) => {
    res.status(204).end();
  });

Middleware на уровне маршрута

// Middleware только для конкретного маршрута
function validateId(req, res, next) {
  const id = Number(req.params.id);
  if (isNaN(id) || id < 1) {
    return res.status(400).json({ error: 'Invalid ID' });
  }
  req.numericId = id; // добавляем в req для handler-а
  next;
}

function requireAuth(req, res, next) {
  if (!req.headers.authorization) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  next;
}

// Один middleware
app.get('/api/users/:id', validateId, (req, res) => {
  res.json({ id: req.numericId });
});

// Несколько middleware (массив или через запятую)
app.delete('/api/users/:id', [requireAuth, validateId], (req, res) => {
  res.status(204).end();
});

// Middleware для всех маршрутов роутера
const router = express.Router;
router.use(requireAuth); // все маршруты этого роутера требуют auth
router.get('/profile', (req, res) => { /* ... */ });
router.get('/settings', (req, res) => { /* ... */ });

404 — Not Found

const express = require('express');
const app = express;

// ... все маршруты ...

app.get('/api/users', (req, res) => { /* ... */ });
app.post('/api/users', (req, res) => { /* ... */ });

// 404 handler — ПОСЛЕ всех маршрутов
app.use((req, res) => {
  res.status(404).json({
    error: 'Not Found',
    message: `Route ${req.method} ${req.url} not found`,
    status: 404
  });
});

app.listen(3000);

Полный пример: REST API

// routes/products.js
const express = require('express');
const router = express.Router;

let products = [
  { id: 1, name: 'Laptop', price: 999, category: 'electronics' },
  { id: 2, name: 'Book', price: 15, category: 'education' },
  { id: 3, name: 'Phone', price: 699, category: 'electronics' }
];
let nextId = 4;

// GET /api/products?category=electronics&sort=price&order=desc
router.get('/', (req, res) => {
  let result = [...products];

  // Фильтрация
  if (req.query.category) {
    result = result.filter(p => p.category === req.query.category);
  }

  // Сортировка
  if (req.query.sort()) {
    const field = req.query.sort();
    const order = req.query.order === 'desc' ? -1 : 1;
    result.sort((a, b) => (a[field] > b[field] ? order : -order));
  }

  res.json({ count: result.length, data: result });
});

// GET /api/products/:id
router.get('/:id', (req, res) => {
  const product = products.find(p => p.id === Number(req.params.id));
  if (!product) {
    return res.status(404).json({ error: 'Product not found' });
  }
  res.json(product);
});

// POST /api/products
router.post('/', (req, res) => {
  const { name, price, category } = req.body;

  if (!name || !price) {
    return res.status(400).json({ error: 'Name and price are required' });
  }

  const product = { id: nextId++, name, price: Number(price), category };
  products.push(product);
  res.status(201).json(product);
});

// PATCH /api/products/:id
router.patch('/:id', (req, res) => {
  const product = products.find(p => p.id === Number(req.params.id));
  if (!product) {
    return res.status(404).json({ error: 'Product not found' });
  }

  const { name, price, category } = req.body;
  if (name) product.name = name;
  if (price) product.price = Number(price);
  if (category) product.category = category;

  res.json(product);
});

// DELETE /api/products/:id
router.delete('/:id', (req, res) => {
  const index = products.findIndex(p => p.id === Number(req.params.id));
  if (index === -1) {
    return res.status(404).json({ error: 'Product not found' });
  }
  products.splice(index, 1);
  res.status(204).end();
});

module.exports = router;

Частые ошибки

  1. Порядок маршрутов/users/me после /users/:id никогда не вызовется, me попадёт в :id
  2. Забывают return — после res.status(400).json(...) код продолжает выполняться, вызывая двойной ответ
  3. req.params.id — строкаreq.params.id === 42 всегда false, нужно Number(req.params.id)
  4. Не подключают express.json()req.body равен undefined для POST/PUT/PATCH
  5. Путают req.params и req.query/users/:id = params, /users?id=42 = query
  6. Не используют Router — все маршруты в одном файле, проект становится неподдерживаемым

Практика

  1. Создать CRUD-маршруты для ресурса /api/products через express.Router
  2. Добавить фильтрацию по query-параметрам (category, minPrice, maxPrice)
  3. Создать вложенный роутер: /api/users/:userId/orders
  4. Реализовать middleware validateId, проверяющий что :id — число
  5. Добавить 404-handler для несуществующих маршрутов

Связанные темы

Ресурсы