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

Express — middleware, маршруты и ошибки

Разработчику

Предварительные знания

Express — это минималистичный и гибкий веб-фреймворк для Node.js, который предоставляет разработчикам мощный набор инструментов для создания веб-приложений, RESTful API и серверных приложений различной сложности, будучи по сути тонкой надстройкой над встроенным модулем HTTP, значительно упрощающей обработку запросов и формирование ответов. Express был создан Тиджеем Холлоуэем и быстро стал де-факто стандартом в экосистеме Node.js благодаря своей простоте, невероятной расширяемости и огромному сообществу, которое разработало тысячи middleware-пакетов для решения практически любой задачи — от аутентификации и работы с сессиями до сжатия данных и защиты от уязвимостей. Основная философия Express заключается в том, чтобы быть ненавязчивым и предоставлять базовый каркас, на котором разработчик может строить приложение по своему усмотрению, без навязывания жестких архитектурных паттернов, что выгодно отличает его от более тяжеловесных фреймворков, таких как Ruby on Rails или Django. В основе Express лежит система маршрутизации, которая позволяет определять обработчики для различных HTTP-методов (GET, POST, PUT, DELETE и другие) и URL-путей, а также мощная система middleware, дающая возможность выстраивать гибкие цепочки обработки запросов для выполнения сквозных задач, таких как логирование, авторизация, парсинг данных и обработка ошибок. Несмотря на свою легкость, Express обеспечивает все необходимые возможности для создания полноценных продакшен-приложений, включая управление сессиями, работу с куки, обработку статических файлов, поддержку шаблонизаторов и интеграцию с различными базами данных, при этом оставаясь достаточно быстрым и производительным благодаря асинхронной природе Node.js. Экосистема Express настолько обширна, что практически любой необходимый функционал уже реализован в виде отдельного пакета, который можно подключить одной командой, что делает этот фреймворк идеальным выбором как для начинающих разработчиков, осваивающих серверную разработку на JavaScript, так и для опытных команд, создающих сложные высоконагруженные системы.

Нужен опыт из первой программы на Node.js: app.get, app.post, req.body. Когда эндпоинтов больше пяти, один server.js превращается в "простыню". Ниже — как разложить сервер по папкам, пропустить запрос через middleware, настроить CORS для браузерного фронта и единый формат ошибок.

Склейка с React/Vue/Next: Fullstack. Идеи цепочки middleware и роутеров переносятся на Fastify, Hono и другие фреймворки.

Рекомендуемый порядок изучения

  1. Закрепить мини-API из Первая программа на Node.js с Router и errorHandler.
  2. Подключить клиент на React/Vue/Next и отладить CORS/прокси по Fullstack на JavaScript — API и фронтенд.
  3. При необходимости production — встроенные модули и CLI / деплой.

Как Express обрабатывает один запрос

HTTP-запрос проходит цепочку функций. Middleware может прочитать тело, поставить заголовок, вызвать next() и передать управление дальше или завершить ответ через res.json / res.status — тогда остальные обработчики для этого запроса обычно не вызываются.

Обработчик app.get('/notes', …) — тоже middleware, только последний в своей ветке: он отправляет ответ клиенту.

ОбъектЧто внутри (упрощённо)
reqmethod, url, headers, body, params (:id), query (?page=1)
resstatus(), json(), send() — формирование ответа
nextПереход к следующему middleware; next(err) — прыжок в обработчик ошибок

Структура папок для учебного API

notes-api/
server.js # создание app, listen
routes/
notes.js # Router для /notes
health.js # GET /health
middleware/
errorHandler.js
notFound.js
store/
memoryStore.js # логика данных (потом — БД)

server.js остаётся тонким: только app.use(...), подключение роутеров, глобальные middleware. Бизнес-логика заметок — в store/ или сервисах.


Router и префиксы

Router — это встроенный механизм Express, который позволяет создавать модульные, изолированные группы маршрутов, объединенные общим префиксом пути или общей логикой, что дает возможность организовать код приложения в независимые подсистемы, каждая из которых отвечает за свою функциональную область, например, отдельные роутеры для пользователей, товаров, заказов или административной панели. Роутер в Express является полноценным middleware-компонентом, который ведет себя как миниатюрное приложение внутри основного приложения, имеющее собственную систему маршрутизации, собственные middleware и собственную логику обработки ошибок, но при этом оно не является независимым процессом и всегда подключается к главному экземпляру Express через метод app.use(). Создание роутера осуществляется с помощью вызова express.Router(), после чего на полученном объекте можно определять маршруты точно так же, как на основном приложении, используя методы router.get(), router.post(), router.put(), router.delete() и другие, а также применять к этому роутеру локальные middleware, которые будут действовать исключительно внутри его области видимости. Основное преимущество использования роутеров заключается в возможности выносить логику работы с различными сущностями в отдельные файлы, что кардинально улучшает читаемость и поддерживаемость кода, особенно когда приложение разрастается до десятков или сотен эндпоинтов, а также позволяет переиспользовать целые группы маршрутов в разных проектах или версиях API. Роутеры могут быть вложенными, то есть один роутер может использовать другой роутер, создавая древовидную структуру маршрутов, что особенно полезно при реализации иерархических ресурсов, например, "/api/v1/users/:userId/posts", где сначала обрабатывается роутер пользователей, а внутри него — роутер постов. Кроме того, роутеры поддерживают параметризованные пути, валидацию параметров и обработку специфических ошибок, что делает их самостоятельными и законченными строительными блоками для архитектуры любого Express-приложения.

Префиксы — это общие начальные сегменты URL-путей, которые используются в Express для группировки логически связанных маршрутов под единым корневым путем, что позволяет структурировать API, упрощать навигацию по эндпоинтам и избегать конфликтов имен при подключении множества роутеров к одному приложению. В контексте Express префиксы задаются первым аргументом метода app.use() или app.METHOD(), например, app.use('/api/users', userRouter), где '/api/users' является префиксом, и все маршруты, определенные внутри роутера userRouter, будут автоматически дополняться этим префиксом, превращая, скажем, маршрут router.get('/profile') в полноценный путь '/api/users/profile'. Использование префиксов является фундаментальным паттерном организации кода в Express, поскольку оно позволяет разработчику четко разделять различные версии API, например, '/api/v1' и '/api/v2', или различные логические блоки, такие как '/admin' для административной панели и '/public' для общедоступных эндпоинтов, не смешивая их обработчики в одном файле. Префиксы также играют важную роль в обеспечении обратной совместимости, когда при изменении структуры API можно оставить старые маршруты с одним префиксом и добавить новые с другим, давая клиентам время на миграцию без поломки существующего функционала. Кроме того, префиксы часто используются совместно с middleware, позволяя применять определенные обработчики только к группе маршрутов, например, middleware для проверки аутентификации применяется ко всему префиксу '/api', а middleware для логирования — ко всему приложению, что обеспечивает тонкий контроль над обработкой запросов на разных уровнях иерархии. Важно понимать, что префиксы могут быть не только статическими строками, но и содержать динамические параметры, например, '/users/:userId', что позволяет строить гибкие и выразительные маршруты, способные обрабатывать ресурсы с переменными идентификаторами, при этом параметры из префикса автоматически становятся доступными в объекте req.params для всех вложенных маршрутов.

express.Router() — мини-приложение со своими маршрутами. Его монтируют с префиксом:

// routes/health.js

import { Router } from 'express';

const router = Router();
router.get('/', (_req, res) => {
res.json({ status: 'ok', uptime: process.uptime() });
});
export default router;

Разбор:

  • Router() создаёт изолированный роутер, чтобы не держать все обработчики в server.js.
  • router.get('/', ...) описывает обработчик для корневого пути внутри этого роутера.
  • _req с подчёркиванием показывает, что параметр запроса в этой функции не используется.
  • res.json(...) отправляет JSON и автоматически выставляет Content-Type: application/json.
  • process.uptime() возвращает время жизни процесса в секундах, поэтому endpoint удобен как health-check.
// server.js

import healthRouter from './routes/health.js';

app.use('/health', healthRouter); // итоговый путь: GET /health

Разбор:

  • import healthRouter ... подключает модуль с маршрутами как отдельную ответственность.
  • app.use('/health', healthRouter) монтирует роутер на префикс, то есть все пути роутера получают начало /health.
  • Путь '/' внутри роутера после монтирования становится GET /health.
  • Такой подход упрощает масштабирование: можно добавить usersRouter, authRouter по той же схеме.

Путь в роутере '/' + префикс '/health' = GET /health. Для заметок: app.use('/notes', notesRouter) и внутри роутера router.get('/')GET /notes.

Плюсы — проще читать, тестировать supertest-ом по модулю, вынести группу в отдельный микросервис позже.


CORS — когда фронт в браузере

CORS — это акроним от Cross-Origin Resource Sharing, что в переводе означает «совместное использование ресурсов между разными источниками», и представляет собой механизм безопасности, реализованный в браузерах и основанный на HTTP-заголовках, который позволяет серверу явно разрешать или запрещать веб-страницам, загруженным с одного домена, отправлять запросы к ресурсам, расположенным на другом домене, порту или протоколе, что известно как междоменные запросы. По умолчанию браузеры реализуют политику одинакового источника, которая строго запрещает скриптам с одного сайта взаимодействовать с ресурсами другого сайта, и CORS был создан именно для того, чтобы безопасно обойти это ограничение, предоставляя серверам возможность контролировать, какие именно источники имеют доступ к их данным, а какие нет. В контексте Express для работы с CORS чаще всего используется популярный пакет cors, который представляет собой middleware, добавляющий в ответы сервера необходимые заголовки, такие как Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, и обрабатывающий предварительные OPTIONS-запросы, которые браузер автоматически отправляет перед сложными запросами, чтобы убедиться, что сервер разрешает междоменное взаимодействие. Настройка CORS в Express может быть как глобальной, применяемой ко всем маршрутам приложения с помощью app.use(cors()), так и локальной, применяемой только к конкретным маршрутам или группам маршрутов, причем можно гибко конфигурировать список разрешенных источников, разрешенные методы, разрешенные заголовки, а также возможность передачи учетных данных, таких как куки или заголовки авторизации. Особую важность CORS приобретает в эпоху микросервисной архитектуры и разделения фронтенда и бэкенда, когда клиентское приложение, работающее, например, на localhost:3000, должно взаимодействовать с серверным API, запущенным на localhost:5000, или когда публичное API предназначено для использования сторонними разработчиками с совершенно разных доменов. Правильная настройка CORS критически важна для безопасности, поскольку слишком либеральная политика (разрешающая все источники) может сделать приложение уязвимым для атак, в то время как слишком строгая политика может полностью блокировать легитимные запросы, поэтому разработчик должен тщательно балансировать между удобством использования и требованиями безопасности в зависимости от конкретного сценария работы приложения.

Origin — схема + хост + порт. Для браузера http://localhost:5173 и http://localhost:3000разные origin, даже на одной машине.

Браузер по правилам безопасности блокирует ответ API, если сервер не вернул заголовки CORS (Access-Control-Allow-Origin и др.). curl и Postman CORS не проверяют — отсюда типичная путаница: "в Postman работает, в React — нет". Сначала повторите URL в терминале — curl / fetch — примеры (раздел про CORS); готовые шаблоны fetch и отладка в браузере — Fetch / axios — типовые запросы; компонент списка с API в React — React — компоненты-рецепты, затем правьте заголовки на Express.

npm install cors

Разбор:

  • npm install cors добавляет пакет в dependencies, чтобы middleware был доступен в runtime.
  • После установки модуль cors можно подключить через import cors from 'cors'.
  • CORS-конфигурация применяется централизованно через app.use(...).

import cors from 'cors';

app.use(cors({
origin: ['http://localhost:5173', 'http://127.0.0.1:5173'],
methods: ['GET', 'POST', 'DELETE'],
}));

Разбор:

  • app.use(cors(...)) включает CORS-мидлварь для всех маршрутов, подключённых после этой строки.
  • origin задаёт белый список источников, которым браузер разрешит читать ответы API.
  • methods формирует политику для preflight-запроса OPTIONS.
  • Если origin пользователя отсутствует в массиве, браузер заблокирует ответ даже при корректной логике API.
ПараметрСмысл
originСписок адресов фронта, которым разрешён доступ
methodsКакие HTTP-методы разрешены в preflight

В production укажите реальный домен фронта. Для cookie с credentials: true нельзя ставить origin: '*' — нужен конкретный домен.

Прокси в Vite — альтернатива: браузер ходит на localhost:5173/api/..., dev-сервер пересылает на :3000. Тогда CORS в API в dev можно не открывать — см. Fullstack на JavaScript — API и фронтенд.


Обработка ошибок

Проблема с async

В async (req, res) => { … } необработанный reject Promise может не попасть в ваш errorHandler. Обёртка передаёт ошибку в next(err):

export const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};

router.get('/:id', asyncHandler(async (req, res) => {
const note = await store.find(req.params.id);
if (!note) return res.status(404).json({ error: 'not found' });
res.json(note);
}));

Разбор:

  • asyncHandler принимает исходный обработчик и возвращает новый middleware.
  • Promise.resolve(fn(...)) унифицирует поведение для sync/async функций.
  • .catch(next) гарантированно пробрасывает исключение в центральный errorHandler.
  • req.params.id читает параметр маршрута из /:id.
  • Ранний return в if (!note) завершает текущий обработчик и не даёт отправить второй ответ.

Центральный error middleware

Подключается после всех маршрутов. Сигнатура ровно четыре аргумента — Express по этому понимает, что это обработчик ошибок:

// middleware/errorHandler.js
export function errorHandler(err, req, res, _next) {
console.error(err);
const status = err.status ?? 500;
res.status(status).json({
error: err.message ?? 'Internal Server Error',
});
}

Разбор:

  • Первый параметр err плюс четыре аргумента — обязательный формат error-middleware в Express.
  • console.error(err) фиксирует полный объект ошибки для логов и диагностики.
  • err.status ?? 500 выставляет код по умолчанию, если бизнес-ошибка не задала свой статус.
  • res.status(...).json(...) возвращает единый формат ошибки для фронтенда.

import { notFound } from './middleware/notFound.js';
import { errorHandler } from './middleware/errorHandler.js';

app.use(notFound); // 404 для неизвестных путей
app.use(errorHandler); // всё, что пришло через next(err)

Разбор:

  • Порядок критичен: сначала notFound, потом errorHandler.
  • notFound срабатывает, когда ни один роут не обработал запрос.
  • errorHandler обрабатывает ошибки из next(err) и из middleware выше по цепочке.
  • Если поменять их местами, часть ошибок и 404 будет обрабатываться некорректно.

middleware/notFound.js:

export function notFound(req, res, next) {
res.status(404).json({ error: `Route ${req.method} ${req.url} not found` });
}

Разбор:

  • Middleware получает req.method и req.url, чтобы вернуть точный контекст ошибки.
  • res.status(404) задаёт HTTP-статус "маршрут не найден".
  • json(...) формирует предсказуемый ответ, который удобно показывать в UI и логировать.
  • next здесь не вызывается, потому что ответ уже отправлен.

В бизнес-коде можно бросать осмысленную ошибку:

const err = new Error('text is required');
err.status = 400;
throw err;

Разбор:

  • new Error(...) создаёт объект ошибки с читаемым сообщением.
  • err.status = 400 добавляет HTTP-код для клиентской ошибки валидации.
  • throw err прерывает обычный поток и передаёт управление в errorHandler.
  • Такой паттерн сохраняет бизнес-логику чистой, без дублирования res.status(...).json(...) в каждом месте.

?? — "если слева null или undefined, возьми справа"; удобно для кода по умолчанию.


Валидация тела запроса

Валидация тела запроса — это критически важный процесс проверки и очистки данных, которые клиент передает серверу в теле HTTP-запроса, обычно в формате JSON, XML или URL-кодированных данных, с целью убедиться, что эти данные соответствуют ожидаемой структуре, типам, форматам и бизнес-правилам, прежде чем они будут использованы в логике приложения или сохранены в базу данных. В экосистеме Express валидация тела запроса чаще всего реализуется через специализированные middleware-библиотеки, такие как Joi, Yup, Zod или классный validator, либо через встроенные возможности некоторых фреймворков, и обычно выполняется после парсинга тела запроса с помощью middleware express.json() или express.urlencoded(), которые преобразуют входящий поток данных в доступный объект req.body. Процесс валидации включает в себя множество аспектов: проверка наличия обязательных полей, соответствие типов данных (строка, число, булево значение, дата, массив, объект), проверка формата (электронная почта, URL, телефонный номер, дата), проверка длины строк, минимальных и максимальных значений чисел, проверка на соответствие допустимому набору значений (enum), а также более сложные бизнес-правила, такие как проверка того, что дата окончания не раньше даты начала, или что сумма заказа не превышает баланс пользователя. Помимо проверки корректности, валидация также включает санитизацию — процесс очистки данных от потенциально опасных символов или скриптов, что является важной защитой от атак типа XSS (межсайтовый скриптинг) и SQL-инъекций, особенно когда данные впоследствии вставляются в HTML-шаблоны или SQL-запросы. Правильно организованная валидация тела запроса является краеугольным камнем безопасности и надежности любого веб-приложения, поскольку она не только предотвращает ошибки выполнения, связанные с некорректными данными, но и служит первым рубежом обороны против злонамеренных пользователей, пытающихся передать на сервер вредоносные данные, а также значительно улучшает пользовательский опыт, предоставляя понятные и информативные сообщения об ошибках при неверном заполнении форм. В современных практиках разработки валидация часто выносится в отдельные схемы или контракты, которые описывают ожидаемую структуру данных для каждого эндпоинта, что позволяет переиспользовать эти схемы как на сервере, так и на клиенте для единообразной проверки данных на всех уровнях приложения.

Ручная проверка из Первая программа на Node.js (if (!text)) достаточна для старта. При росте API удобна схема Zod:

npm install zod

Разбор:

  • Команда устанавливает библиотеку схемной валидации zod.
  • После установки можно описывать контракты запросов декларативно.
  • Это уменьшает количество ручных if и повышает читаемость API-валидации.

Код ITЗагрузка примера кода…

Разбор:

  • z.object({...}) задаёт точную форму тела запроса.
  • z.string().trim().min(1).max(2000) последовательно нормализует и валидирует поле text.
  • safeParse(req.body) возвращает объект результата вместо исключения.
  • При !parsed.success сервер отдаёт 400 с деталями ошибок схемы.
  • parsed.data содержит уже проверенные и приведённые данные, безопасные для бизнес-логики.

safeParse возвращает { success: true, data } или { success: false, error } без выброса исключения — проще отдать 400 клиенту.


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

СимптомПричинаРешение
CORS только в PostmanБраузерная политикаcors или прокси Vite
Cannot set headers after they are sentДважды res.json или нет return после ошибкиПосле res.status(400).json(...)return
404 на всех путяхRouter без префикса или лишний префиксСверить app.use('/notes', router) и пути внутри
Stack trace у клиентаВ production в JSON только error, детали — в лог

Что попробовать

  1. Пакет helmet — базовые заголовки безопасности одной строкой app.use(helmet()).
  2. Разные ответы при NODE_ENV=development и production.
  3. Тесты supertest на GET /health и POST /notesТестирование JavaScript — Vitest и Testing Library.

Связанные материалы


Шаблон middleware-цепочки для командной разработки

Когда над API работают несколько человек, полезно заранее договориться о порядке middleware. Это убирает скрытые баги и ускоряет отладку.


Рекомендуемый порядок подключения

  1. Технические middleware: requestId, логирование.
  2. Безопасность — helmet, cors, лимит тела запроса.
  3. Парсинг: express.json.
  4. Бизнес-маршруты.
  5. notFound для неизвестных путей.
  6. errorHandler для централизованной обработки ошибок.

Единый формат ошибки для фронтенда

ПолеНазначение
codeМашинный код ошибки (validation_error)
messageТекст для пользователя или лога
requestIdИдентификатор запроса для трассировки

Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.