Какие архитектурные решения Express.js задаёт вокруг request lifecycle, middleware, dependency model, validation и error handling?
Express строит весь pipeline на цепочке middleware (req, res, next). Нет встроенного DI, валидации и структуры — всё подключается вручную; порядок регистрации и обработка async-ошибок — зона наибольшего риска.
Request lifecycle в Express
Каждый входящий HTTP-запрос проходит через цепочку middleware функций. Каждая функция получает объекты req, res и коллбэк next. Вызов next() передаёт управление следующему middleware; вызов next(err) — пропускает все обычные middleware и передаёт управление error-обработчику.
// Упрощённая модель жизненного цикла
const app = express();
// 1. Application-level middleware (выполняется для каждого запроса)
app.use((req, res, next) => {
req.startTime = Date.now();
next();
});
// 2. Парсинг тела запроса
app.use(express.json({ limit: "1mb" }));
// 3. Router-level middleware
const router = express.Router();
router.use((req, res, next) => {
if (!req.headers.authorization) {
return next(new Error("Unauthorized"));
}
next();
});
// 4. Route handler
router.get("/items/:id", async (req, res, next) => {
try {
const item = await db.find(req.params.id);
res.json(item);
} catch (err) {
next(err);
}
});
app.use("/api", router);
// 5. Error handler (обязательно 4 параметра)
app.use((err, req, res, next) => {
const status = err.status ?? 500;
res.status(status).json({ message: err.message });
});
Middleware как основной строительный блок
Express не разделяет middleware на типы на уровне API — любая функция (req, res, next) является middleware. Порядок регистрации через app.use() и router.use() определяет порядок выполнения. Это даёт гибкость, но требует строгой дисциплины:
app.use(path, handler)— монтирует middleware на префикс пути.router.use()— локальный scope для группы маршрутов.- Middleware могут быть переиспользованы как npm-пакеты:
morgan,helmet,cors,compression.
Dependency model
Express не имеет встроенного DI-контейнера. Зависимости (БД, сервисы) обычно внедряются одним из трёх способов:
- Closure factories — фабричная функция принимает зависимости и возвращает router.
- app.locals / req.app.locals — антипаттерн для хранения глобального состояния.
- Сторонний DI — awilix или tsyringe для продвинутых случаев.
// Рекомендуемый pattern: closure factory
export function createUserRouter(userService) {
const router = express.Router();
router.get("/", async (req, res, next) => {
try {
res.json(await userService.listAll());
} catch (err) {
next(err);
}
});
return router;
}
// В точке сборки приложения:
const userService = new UserService(db);
app.use("/users", createUserRouter(userService));
Валидация
Express не включает валидацию — это ответственность разработчика. Стандартные подходы:
- Zod — декларативная схема с полным выводом TypeScript-типов.
- express-validator — цепочки валидации прямо в определении маршрута.
- Joi — зрелая библиотека с богатым API для сложных схем.
Error handling
Обработчик ошибок — это middleware с четырьмя параметрами, зарегистрированный последним. Важно централизовать его, а не разбрасывать res.status(500) по всем обработчикам.
Подводные камни
- Async-обработчики без try/catch и без express-async-errors оставят запрос висеть вечно — promise rejection в Express 4 не обрабатывается автоматически.
- Error middleware распознаётся только по числу аргументов (ровно 4). Если написать
(err, req, res) =>, Express воспримет его как обычный middleware. - app.use() без пути перехватывает все методы и все пути — случайное размещение ответа до нужного маршрута ломает логику.
- req.body равен undefined без парсера — забыть express.json() — распространённая ошибка новичков.
- Хранение состояния в замыканиях router-файлов делает модули синглтонами; это ломается при горячей перезагрузке или тестировании без сброса require-кэша.
- next() вызванный после res.send() вызывает ошибку «headers already sent» — нужно всегда делать return next() или return res.json().
- Отсутствие встроенного rate-limiting, CORS и helmet — забыть подключить хотя бы helmet() оставляет сервис уязвимым.
- Порядок: router.param() выполняется до route handler, это неочевидно и ломает ожидания при отладке.
What hurts your answer
- Знать термины Express.js, но не понимать связи между абстракциями
- Объяснять поведение через отдельные примеры вместо причинной модели
- Не связывать mental model с диагностикой ошибок
What they're listening for
- Понимает ключевые абстракции Express.js
- Может предсказывать поведение системы через mental model
- Связывает модель с debugging и production decisions