Middleware для Express и Fastify

Роль схем валидации в промежуточном слое

Промежуточный слой (middleware) в серверных приложениях выполняет функцию контроля и преобразования входящих данных до того, как они попадут в бизнес-логику. Использование схем валидации позволяет формализовать контракт между клиентом и сервером, а также централизовать обработку ошибок и приведение типов.

Библиотека Zod предоставляет декларативный способ описания структуры данных с автоматической проверкой и выводом типов TypeScript. В контексте middleware она выступает как слой строгой типизации и валидации входящих HTTP-запросов.

Ключевое свойство подхода — единый источник истины для данных запроса:

  • тело запроса (body)
  • параметры маршрута (params)
  • query-параметры (query)
  • заголовки (headers)

Базовый паттерн middleware с Zod

Типовая структура middleware с валидацией выглядит как функция, принимающая схему и возвращающая обработчик запроса.

import { ZodSchema } fr om "zod";

export function validate(schema: ZodSchema) {
  return (req, res, next) => {
    const result = schema.safeParse({
      body: req.body,
      query: req.query,
      params: req.params,
    });

    if (!result.success) {
      return res.status(400).json({
        message: "Validation error",
        errors: result.error.flatten(),
      });
    }

    req.validated = result.data;
    next();
  };
}

Структура safeParse используется для предотвращения исключений и обеспечения контролируемого потока ошибок.


Композиция схем для Express

В Express часто требуется раздельная валидация частей запроса. Zod позволяет объединять схемы через object и merge, формируя единый контракт.

import { z } from "zod";

const paramsSchema = z.object({
  id: z.string().uuid(),
});

const bodySchema = z.object({
  title: z.string().min(3),
  content: z.string(),
});

const querySchema = z.object({
  debug: z.string().optional(),
});

export const requestSchema = z.object({
  params: paramsSchema,
  body: bodySchema,
  query: querySchema,
});

Middleware слой может использовать такую структуру без дополнительной логики разбиения.


Типизация req объекта

Zod позволяет извлекать типы напрямую из схемы:

import { z } from "zod";

type RequestData = z.infer<typeof requestSchema>;

Расширение объекта запроса:

declare global {
  namespace Express {
    interface Request {
      validated?: RequestData;
    }
  }
}

Такой подход связывает runtime-валидацию и compile-time типизацию, устраняя дублирование типов.


Обработка ошибок Zod в Express

Zod возвращает структурированные ошибки, содержащие путь до некорректного поля и описание нарушения.

import { ZodError } from "zod";

function formatZodError(error: ZodError) {
  return error.errors.map(e => ({
    path: e.path.join("."),
    message: e.message,
  }));
}

Middleware обработки ошибок:

app.use((err, req, res, next) => {
  if (err instanceof ZodError) {
    return res.status(400).json({
      errors: formatZodError(err),
    });
  }

  next(err);
});

Централизация обработки ошибок позволяет избегать дублирования логики в каждом роуте.


Middleware фабрика для Express

Расширенный вариант middleware учитывает выборочную валидацию частей запроса.

import { ZodSchema } from "zod";

interface SchemaMap {
  body?: ZodSchema;
  query?: ZodSchema;
  params?: ZodSchema;
}

export function validateRequest(schemaMap: SchemaMap) {
  return (req, res, next) => {
    const data: any = {};

    if (schemaMap.body) {
      const parsed = schemaMap.body.safeParse(req.body);
      if (!parsed.success) return res.status(400).json(parsed.error);
      data.body = parsed.data;
    }

    if (schemaMap.query) {
      const parsed = schemaMap.query.safeParse(req.query);
      if (!parsed.success) return res.status(400).json(parsed.error);
      data.query = parsed.data;
    }

    if (schemaMap.params) {
      const parsed = schemaMap.params.safeParse(req.params);
      if (!parsed.success) return res.status(400).json(parsed.error);
      data.params = parsed.data;
    }

    req.validated = data;
    next();
  };
}

Такой подход снижает связанность и повышает повторное использование схем.


Fastify и встроенная модель схем

Fastify изначально ориентирован на схемы и использует их для валидации и сериализации. Zod может интегрироваться через preHandler или адаптеры.

Базовый вариант через preHandler:

import { z } from "zod";

const schema = z.object({
  id: z.string(),
});

fastify.get("/user/:id", {
  preHandler: (req, reply, done) => {
    const result = schema.safeParse(req.params);

    if (!result.success) {
      reply.status(400).send(result.error.flatten());
      return;
    }

    req.params = result.data;
    done();
  },
}, async (req, reply) => {
  return { id: req.params.id };
});

Интеграция Zod как схема Fastify

Fastify позволяет использовать JSON Schema, однако Zod можно адаптировать через преобразование.

import { z } from "zod";

function zodToFastifySchema(schema: z.ZodSchema) {
  return schema; // упрощённый адаптер
}

В реальных системах используется более строгая трансляция через промежуточные библиотеки.


Плагин для Fastify с Zod-валидацией

Плагин может централизовать поведение валидации:

import fp from "fastify-plugin";
import { ZodSchema } from "zod";

export default fp(async (fastify) => {
  fastify.decorate("validateZod", (schema: ZodSchema, value: unknown) => {
    const result = schema.safeParse(value);

    if (!result.success) {
      throw result.error;
    }

    return result.data;
  });
});

Использование внутри маршрутов:

fastify.post("/item", async (req, reply) => {
  const data = fastify.validateZod(bodySchema, req.body);
  return data;
});

Разделение ответственности схем

В крупных приложениях схемы организуются по слоям:

  • DTO схемы (входные данные API)
  • Domain схемы (внутренние структуры)
  • Persistence схемы (БД модели)

Zod используется как трансформатор между слоями:

const apiSchema = z.object({
  price: z.string(),
});

const domainSchema = apiSchema.transform((data) => ({
  price: Number(data.price),
}));

Middleware с трансформацией данных

Zod поддерживает преобразования через transform, что позволяет использовать middleware не только для проверки, но и для нормализации данных.

const schema = z.object({
  page: z.string().transform(Number),
  lim it: z.string().transform(v => Math.min(Number(v), 100)),
});

После валидации данные уже находятся в нужном формате, исключая дополнительную обработку в контроллерах.


Частично валидируемые схемы и optional поля

Гибкость middleware увеличивается за счёт partial:

const updateSchema = z.object({
  title: z.string(),
  content: z.string(),
}).partial();

Это позволяет использовать одну и ту же структуру для разных HTTP методов без дублирования.


Интеграция с типами ответа

Zod позволяет описывать не только входящие данные, но и структуру ответа:

const responseSchema = z.object({
  id: z.string(),
  createdAt: z.date(),
});

Fastify может использовать такие схемы для сериализации и оптимизации ответа, а Express — для унификации контрактов API.


Масштабирование middleware-слоя

При увеличении числа эндпоинтов становится критичным единый подход к:

  • повторному использованию схем
  • централизованной обработке ошибок
  • композиции middleware
  • строгой типизации данных

Zod в связке с middleware решает задачу унификации контрактов между слоями приложения без необходимости введения отдельного DSL или генераторов схем.