Перейти к содержимому

10. Роутинг и параметры URL

Иллюстрация к уроку

Роутинг — это определение, как приложение отвечает на запросы к определённым URL с определённым HTTP методом.

app.get('/users', handler); // Получить список
app.post('/users', handler); // Создать
app.put('/users/:id', handler); // Полная замена
app.patch('/users/:id', handler); // Частичное обновление
app.delete('/users/:id', handler);// Удалить
app.all('/path', handler); // Любой метод
app.options('/path', handler); // Preflight CORS
// :id — обязательный параметр
app.get('/users/:id', (req, res) => {
const { id } = req.params;
res.json({ id, message: `Пользователь ${id}` });
});
// Несколько параметров
app.get('/posts/:year/:month/:slug', (req, res) => {
const { year, month, slug } = req.params;
res.json({ year, month, slug });
// GET /posts/2024/01/hello-world
// → { year: '2024', month: '01', slug: 'hello-world' }
});
// Необязательный параметр с ?
app.get('/files/:name.:ext?', (req, res) => {
const { name, ext = 'txt' } = req.params;
res.json({ name, ext });
});
// Wildcard *
app.get('/public/*', (req, res) => {
res.json({ path: req.params[0] });
// GET /public/images/logo.png → { path: 'images/logo.png' }
});
// GET /users?page=2&limit=10&sort=name&order=asc&role=admin
app.get('/users', (req, res) => {
const {
page = 1,
limit = 10,
sort = 'createdAt',
order = 'desc',
role,
search,
} = req.query;
// Конвертируем строки в числа
const pageNum = parseInt(page, 10);
const limitNum = Math.min(parseInt(limit, 10), 100); // не больше 100
const query = {
page: pageNum,
limit: limitNum,
sort,
order: order === 'asc' ? 'asc' : 'desc',
...(role && { role }),
...(search && { search }),
};
res.json({ query, data: [] });
});
// Массивы в query: ?tags[]=js&tags[]=node или ?tags=js&tags=node
app.get('/articles', (req, res) => {
const tags = [].concat(req.query.tags || []); // всегда массив
res.json({ tags });
});
routes/users.js
const { Router } = require('express');
const router = Router();
// Все роуты здесь будут с префиксом /api/users
router.get('/', getUsers);
router.get('/:id', getUserById);
router.post('/', createUser);
router.put('/:id', updateUser);
router.delete('/:id', deleteUser);
// Цепочка для одного пути
router.route('/:id')
.get(getUserById)
.put(updateUser)
.delete(deleteUser);
module.exports = router;
app.js
const usersRouter = require('./routes/users');
const postsRouter = require('./routes/posts');
const authRouter = require('./routes/auth');
app.use('/api/users', usersRouter);
app.use('/api/posts', postsRouter);
app.use('/api/auth', authRouter);
// controllers/users.js — бизнес-логика отдельно от роутов
const { db } = require('../db');
const getUsers = async (req, res, next) => {
try {
const { page = 1, limit = 10 } = req.query;
const offset = (page - 1) * limit;
const users = await db.users.findMany({
skip: offset,
take: parseInt(limit),
orderBy: { createdAt: 'desc' },
});
const total = await db.users.count();
res.json({
data: users,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit),
},
});
} catch (error) {
next(error); // передаём ошибку в error handler
}
};
const getUserById = async (req, res, next) => {
try {
const id = parseInt(req.params.id);
if (isNaN(id)) {
return res.status(400).json({ error: 'Invalid ID' });
}
const user = await db.users.findUnique({ where: { id } });
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
} catch (error) {
next(error);
}
};
module.exports = { getUsers, getUserById };
// Ручная валидация
app.get('/users/:id', (req, res, next) => {
const id = parseInt(req.params.id);
if (isNaN(id) || id <= 0) {
return res.status(400).json({
error: 'ID должен быть положительным числом',
});
}
req.userId = id;
next();
}, getUserById);
// Через Zod (рекомендуется)
const { z } = require('zod');
const paginationSchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(10),
sort: z.enum(['name', 'email', 'createdAt']).default('createdAt'),
});
app.get('/users', (req, res, next) => {
const result = paginationSchema.safeParse(req.query);
if (!result.success) {
return res.status(400).json({
error: 'Некорректные параметры',
details: result.error.flatten(),
});
}
req.query = result.data;
next();
}, getUsers);
// /users/:userId/posts/:postId/comments
const router = Router({ mergeParams: true });
// mergeParams: true — доступ к параметрам родительского роутера
// routes/comments.js
router.get('/', async (req, res) => {
const { userId, postId } = req.params; // mergeParams нужен!
const comments = await db.comments.findMany({
where: { post: { id: postId, authorId: userId } },
});
res.json(comments);
});
// app.js
const commentsRouter = require('./routes/comments');
app.use('/users/:userId/posts/:postId/comments', commentsRouter);
  1. Создай файл routes/products.js с полным CRUD (GET list, GET one, POST, PUT, DELETE)
  2. Реализуй пагинацию через query параметры ?page=1&limit=10
  3. Добавь валидацию ID параметра (должен быть числом)
  4. Создай вложенные роуты: /categories/:catId/products
  5. Реализуй фильтрацию через query: ?minPrice=100&maxPrice=500&category=electronics