К содержанию
Первый Коммит

Модуль 2

Маршрутизация (Routing)

  • HTTP-методы
  • app.get/post/put/delete
  • Параметры
  • Query
  • express.Router
  • Группировка

#2.1 HTTP-методы

Протокол HTTP определяет методы (verbs), описывающие действие над ресурсом. Express предоставляет методы для каждого из них. В REST API методы соответствуют CRUD-операциям:

  • GET — получить данные (Read). Не изменяет состояние сервера
  • POST — создать новый ресурс (Create). Данные в теле запроса
  • PUT — полностью заменить ресурс (Update). Отправляется весь объект
  • PATCH — частично обновить ресурс. Отправляются только изменённые поля
  • DELETE — удалить ресурс (Delete)
JavaScript
const express = require('express');
const app = express();

// Парсинг JSON из тела запроса
app.use(express.json());

// GET — получить всех пользователей
app.get('/api/users', (req, res) => {
    res.json([{ id: 1, name: 'Алексей' }]);
});

// POST — создать пользователя
app.post('/api/users', (req, res) => {
    const { name, email } = req.body;
    res.status(201).json({ id: 2, name, email });
});

// PUT — заменить пользователя полностью
app.put('/api/users/:id', (req, res) => {
    const { id } = req.params;
    const { name, email } = req.body;
    res.json({ id, name, email });
});

// PATCH — обновить частично
app.patch('/api/users/:id', (req, res) => {
    const { id } = req.params;
    res.json({ id, ...req.body });
});

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

Соответствие методов и CRUD — конвенция REST, и её стоит соблюдать: GET только читает (не меняет состояние, кешируется), POST создаёт, PUT заменяет ресурс целиком, PATCH правит частично, DELETE удаляет. Важна идемпотентность: повторный GET, PUT или DELETE оставляет данные на сервере в том же состоянии (хотя ответ может отличаться: второй DELETE вернёт 404), а повторный POST создаёт ещё одну запись — поэтому для регистрации и оплаты используют POST.

#2.2 Параметры маршрута (Route Params)

Параметры маршрута — динамические части URL, обозначенные двоеточием (:). Они доступны через req.params. Используются для идентификации конкретного ресурса: пользователя по ID, статьи по slug, категории по имени.

JavaScript
// Один параметр
app.get('/api/users/:id', (req, res) => {
    const { id } = req.params; // id = "42"
    res.json({ userId: id });
});
// GET /api/users/42 → { userId: "42" }

// Несколько параметров
app.get('/api/users/:userId/posts/:postId', (req, res) => {
    const { userId, postId } = req.params;
    res.json({ userId, postId });
});
// GET /api/users/5/posts/12 → { userId: "5", postId: "12" }

// Опциональный сегмент — в фигурных скобках (Express 5)
app.get('/api/posts/:year{/:month}', (req, res) => {
    const { year, month } = req.params;
    res.json({ year, month: month || 'all' });
});
// GET /api/posts/2025 → { year: "2025", month: "all" }
// GET /api/posts/2025/03 → { year: "2025", month: "03" }

Важно: параметры всегда строки. Для числовых операций требуется преобразование: Number(req.params.id) или parseInt(req.params.id, 10).

#2.3 Query-параметры

Query-параметры — часть URL после знака ?. Используются для фильтрации, сортировки, пагинации, поиска. Доступны через req.query. В отличие от параметров маршрута, query-параметры необязательны.

JavaScript
// GET /api/users?page=2&limit=10&sort=name&order=asc
app.get('/api/users', (req, res) => {
    const {
        page = 1,       // значение по умолчанию
        limit = 10,
        sort = 'id',
        order = 'asc',
        search = ''
    } = req.query;

    console.log(page);   // "2" (строка!)
    console.log(limit);  // "10"

    // Преобразование типов
    const pageNum = parseInt(page, 10);
    const limitNum = parseInt(limit, 10);

    res.json({
        page: pageNum,
        limit: limitNum,
        sort,
        order,
        search
    });
});

// GET /api/products?category=electronics&minPrice=100&maxPrice=500
app.get('/api/products', (req, res) => {
    const { category, minPrice, maxPrice } = req.query;
    // Фильтрация по параметрам...
    res.json({ category, minPrice, maxPrice });
});

Query-параметры (после ?) — для необязательных уточнений: фильтров, сортировки, пагинации, поиска. Их читают из req.query, удобно задавая значения по умолчанию в деструктуризации ({ page = 1 } = req.query). ⚠️ Как и параметры маршрута, все значения query — строки, для чисел нужен parseInt/Number. Повторяющийся ключ (?tag=a&tag=b) Express отдаёт массивом, поэтому при строгой обработке проверяйте тип значения.

#2.4 express.Router()

По мере роста приложения единый файл со всеми маршрутами становится громоздким. express.Router() создаёт модульный обработчик маршрутов — мини-приложение, которое можно подключить к основному. Каждый ресурс (users, posts, products) выносится в отдельный файл.

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

// Все маршруты относительно точки монтирования
router.get('/', (req, res) => {
    res.json([{ id: 1, name: 'Алексей' }]);
});

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

router.post('/', (req, res) => {
    res.status(201).json(req.body);
});

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

router.delete('/:id', (req, res) => {
    res.status(204).send();
});

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

const usersRouter = require('./routes/users');
const postsRouter = require('./routes/posts'); // пишется по образцу users.js — без этого файла Node остановится с ошибкой Cannot find module

app.use(express.json());

// Монтирование роутеров
app.use('/api/users', usersRouter);  // /api/users + /
app.use('/api/posts', postsRouter);  // /api/posts + /

app.listen(3000);

Теперь router.get('/') в users.js обрабатывает GET /api/users, а router.get('/:id') — GET /api/users/42. Префикс задаётся при монтировании через app.use().

express.Router() — это «мини-приложение»: маршруты ресурса описывают относительно корня ('/', '/:id') в отдельном файле, экспортируют router и монтируют через app.use('/api/users', usersRouter). Так каждый ресурс описан в своём файле, а префикс задаётся в одном месте — стандартная организация реальных Express-приложений (так устроено и демо курса).

#2.5 Цепочка обработчиков (Route Chaining)

Метод app.route() позволяет определить несколько HTTP-методов для одного пути в цепочке. Это удобно, когда один URL принимает разные методы.

JavaScript
// Цепочка для одного пути
app.route('/api/books')
    .get((req, res) => {
        res.json([{ 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).send();
    });

app.route() группирует несколько методов одного пути в цепочку, чтобы не повторять URL в каждом app.get/app.post. Приём влияет только на читаемость — поведение то же, что у отдельных вызовов.

#2.6 Обработка 404

Если ни один маршрут не совпал с запросом, Express по умолчанию отправляет HTML-страницу «Cannot GET /path». Для API лучше вернуть JSON-ответ. Добавьте обработчик 404 после всех маршрутов — он сработает, если ни один предыдущий не ответил.

JavaScript
// Все маршруты выше...
app.use('/api/users', usersRouter);
app.use('/api/posts', postsRouter);

// 404 — маршрут не найден (после всех маршрутов!)
app.use((req, res) => {
    res.status(404).json({
        error: 'Not Found',
        message: `Маршрут ${req.method} ${req.originalUrl} не найден`,
        statusCode: 404
    });
});

app.listen(3000);

Обработчик 404 — это middleware без пути (app.use с одним колбэком), и он обязан стоять после всех маршрутов: Express дойдёт до него, только если ни один маршрут выше не ответил. Отсюда правило порядка в Express: сначала парсеры и общие middleware, затем маршруты, затем 404, и в самом конце — обработчик ошибок (модуль 9).

#2.7 Практика

#Хранилище в памяти: полный пример CRUD

До сих пор все примеры модуля были заглушками: они возвращали жёстко заданный массив, копию req.body или { id: req.params.id }, но ничего не хранили. Настоящее API должно хранить данные и работать с ними. На время обучения простейшее хранилище — массив объектов в памяти процесса: он сбрасывается при перезапуске сервера, но для практики этого достаточно (постоянную базу данных подключим в модуле 6). Ниже — полный роутер, который добавляет, ищет, обновляет и удаляет записи в таком хранилище; сохраните его в routes/users.js и смонтируйте, как показано в разделе 2.4.

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

// Хранилище в памяти: массив объектов и счётчик следующего id
const users = [
    { id: 1, name: 'Алексей', age: 30 },
    { id: 2, name: 'Мария', age: 25 }
];
let nextId = 3;

// GET / — список всех
router.get('/', (req, res) => {
    res.json(users);
});

// GET /:id — одна запись по id (404, если не найдена)
router.get('/:id', (req, res) => {
    const id = Number(req.params.id);            // params — строка, приводим к числу
    const user = users.find(u => u.id === id);
    if (!user) {
        return res.status(404).json({ error: 'Пользователь не найден' });
    }
    res.json(user);
});

// POST / — создать: генерируем id, отвечаем 201
router.post('/', (req, res) => {
    const user = { id: nextId++, name: req.body.name, age: Number(req.body.age) };
    users.push(user);
    res.status(201).json(user);
});

// PUT /:id — заменить запись целиком (404, если не найдена)
router.put('/:id', (req, res) => {
    const id = Number(req.params.id);
    const user = users.find(u => u.id === id);
    if (!user) {
        return res.status(404).json({ error: 'Пользователь не найден' });
    }
    user.name = req.body.name;
    user.age = Number(req.body.age);
    res.json(user);
});

// DELETE /:id — удалить через splice (404, если не найдена)
router.delete('/:id', (req, res) => {
    const id = Number(req.params.id);
    const index = users.findIndex(u => u.id === id);
    if (index === -1) {
        return res.status(404).json({ error: 'Пользователь не найден' });
    }
    users.splice(index, 1);
    res.status(204).send();
});

module.exports = router;

Разберём ключевые приёмы. Одну запись ищем через users.find(u => u.id === id): если find вернул undefined, отвечаем 404 непосредственно из обработчика — это 404 для конкретного ресурса, в отличие от глобального 404 из раздела 2.6, который перехватывает запросы к несуществующим маршрутам. Новый id выдаём счётчиком nextId++ и возвращаем статус 201 Created. Для удаления находим позицию через users.findIndex(...) и вырезаем элемент методом users.splice(index, 1). Важен return перед res.status(404): без него код продолжит выполняться и попытается отправить второй ответ — Express выдаст ошибку «Cannot set headers after they are sent».

#Как проверить POST/PUT/DELETE

Адресная строка браузера отправляет только GET: переходом по ссылке нельзя послать POST, PUT, PATCH или DELETE с телом. Чтобы проверить не-GET маршруты (иначе половина CRUD-API остаётся непроверенной), нужен отдельный HTTP-клиент. Разберём три способа: команду curl в терминале, графическое расширение в VS Code и функцию fetch из консоли браузера.

Первый способ — curl (утилита командной строки, отправляющая запрос любым методом):

Терминал
# создать пользователя: POST с JSON-телом
curl -X POST http://localhost:3000/api/users -H "Content-Type: application/json" -d "{\"name\":\"Иван\",\"age\":40}"

# заменить пользователя целиком: PUT
curl -X PUT http://localhost:3000/api/users/3 -H "Content-Type: application/json" -d "{\"name\":\"Иван\",\"age\":41}"

# удалить: DELETE (тело не нужно)
curl -X DELETE http://localhost:3000/api/users/3

Второй способ — графический клиент в VS Code, самый удобный на Windows. Расширение Thunder Client даёт панель, в которой выбирают метод, вводят URL, открывают вкладку Body → JSON и нажимают кнопку Send. Расширение REST Client позволяет записать запрос текстом в файле .http и запустить его ссылкой Send Request над этим запросом. Оба расширения устанавливаются из вкладки Extensions и не требуют командной строки; ту же задачу решает и отдельное приложение Postman.

Третий способ — функция fetch из консоли браузера. Откройте в браузере любой GET-маршрут своего сервера (например, http://localhost:3000/api/users), нажмите F12 → вкладка Console и на этой же вкладке выполните (так запрос будет с того же адреса, без блокировки CORS):

JavaScript
// отправить POST прямо из консоли браузера
fetch('http://localhost:3000/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: 'Иван', age: 40 })
})
    .then(res => res.json())
    .then(console.log);

#Задание 2.1 — REST API книжного магазина

  • Создайте routes/books.js с express.Router()
  • GET /api/books — список книг (массив в памяти)
  • GET /api/books/:id — одна книга по ID
  • POST /api/books — добавить книгу
  • PUT /api/books/:id — обновить книгу
  • DELETE /api/books/:id — удалить книгу
  • GET /api/books?author=Толстой&genre=роман — фильтрация
  • GET /api/books?page=1&limit=5 — пагинация
  • Обработка 404 для несуществующих маршрутов

#Итоги модуля 2

  • HTTP-методы соответствуют CRUD: GET читает, POST создаёт, PUT заменяет ресурс целиком, PATCH правит частично, DELETE удаляет; GET, PUT и DELETE идемпотентны, а повторный POST создаёт ещё одну запись.
  • Статус-коды — часть контракта API: 200 для GET, 201 Created после POST, 204 No Content для DELETE; без app.use(express.json()) тело POST/PUT не попадёт в req.body.
  • Параметры маршрута объявляются двоеточием (/api/users/:id) и читаются из req.params; поддерживаются несколько параметров в одном пути и опциональные сегменты — в Express 5 их берут в фигурные скобки (/api/posts/:year{/:month}).
  • Все значения req.params и req.query — строки; для числовых операций обязательны Number или parseInt.
  • Порядок маршрутов критичен: Express идёт сверху вниз и берёт первый совпавший, поэтому статические пути (/users/new) объявляют выше динамических (/users/:id).
  • Query-параметры после знака ? — для необязательных уточнений: фильтров, сортировки, пагинации, поиска; читаются из req.query, значения по умолчанию удобно задавать в деструктуризации.
  • Повторяющийся query-ключ (?tag=a&tag=b) Express отдаёт массивом — при строгой обработке проверяйте тип значения.
  • express.Router() — мини-приложение для одного ресурса: маршруты описываются относительно корня в отдельном файле, экспортируются и монтируются через app.use('/api/users', usersRouter); префикс задаётся в одном месте.
  • app.route(путь) группирует несколько методов одного URL в цепочку .get().post() — чистая косметика для читаемости, поведение не меняется.
  • Обработчик 404 — app.use с одним колбэком после всех маршрутов: срабатывает, только если никто выше не ответил, и для API возвращает JSON вместо HTML-страницы Cannot GET; общий порядок: парсеры → маршруты → 404 → обработчик ошибок.
  • Хранилище в памяти для практики — массив объектов: поиск find/findIndex по id с приведением Number(req.params.id), добавление push с генерацией id и статусом 201, обновление найденной записи, удаление splice; если элемента нет — верните 404 непосредственно из обработчика (404 для конкретного ресурса в дополнение к глобальному) и обязательно поставьте return, чтобы ответ не был отправлен дважды.
  • Не-GET запросы (POST/PUT/PATCH/DELETE с телом) браузер из адресной строки не отправляет — проверяйте их HTTP-клиентом: командой curl, расширениями Thunder Client или REST Client в VS Code либо fetch из консоли; в Windows PowerShell curl — это псевдоним Invoke-WebRequest, поэтому вызывайте curl.exe или используйте графический клиент.

#Домашнее задание

Постройте с нуля API кулинарной книги — те же приёмы маршрутизации, но на другом ресурсе.

  • Создайте проект recipe-api и роутер routes/recipes.js через express.Router(), смонтируйте его на /api/recipes.
  • Реализуйте CRUD с корректными статус-кодами: GET /api/recipes (список из массива в памяти), GET /api/recipes/:id (404, если нет), POST (201), PUT /api/recipes/:id, DELETE /api/recipes/:id (204).
  • Добавьте фильтрацию через query: /api/recipes?cuisine=итальянская&maxTime=30 (кухня и максимальное время приготовления) со значениями по умолчанию.
  • Добавьте статический маршрут GET /api/recipes/popular (три рецепта с лучшим рейтингом) и убедитесь, что он объявлен раньше /:id — запрос /popular не должен попадать в параметр id.
  • Перепишите маршруты одного пути цепочками router.route('/') и router.route('/:id').
  • (со звёздочкой) Реализуйте вложенный ресурс: GET /api/recipes/:id/ingredients и POST /api/recipes/:id/ingredients, а также JSON-обработчик 404 с полями error, method и url.

Войдите, чтобы отмечать пройденные модули и видеть прогресс по курсу.

Введите запрос: название модуля, тема или раздел.

Стрелки — выбор, Enter — открыть. Ищет по названиям модулей, темам и разделам.