Express.jsMiddleExperience

Какие архитектурные решения 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

Related topics