Промежуточный слой (middleware) в серверных приложениях выполняет функцию контроля и преобразования входящих данных до того, как они попадут в бизнес-логику. Использование схем валидации позволяет формализовать контракт между клиентом и сервером, а также централизовать обработку ошибок и приведение типов.
Библиотека Zod предоставляет декларативный способ описания структуры данных с автоматической проверкой и выводом типов TypeScript. В контексте middleware она выступает как слой строгой типизации и валидации входящих HTTP-запросов.
Ключевое свойство подхода — единый источник истины для данных запроса:
Типовая структура 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 часто требуется раздельная валидация частей запроса. 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 слой может использовать такую структуру без дополнительной логики разбиения.
Zod позволяет извлекать типы напрямую из схемы:
import { z } from "zod";
type RequestData = z.infer<typeof requestSchema>;
Расширение объекта запроса:
declare global {
namespace Express {
interface Request {
validated?: RequestData;
}
}
}
Такой подход связывает runtime-валидацию и compile-time типизацию, устраняя дублирование типов.
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 учитывает выборочную валидацию частей запроса.
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 изначально ориентирован на схемы и использует их для валидации и сериализации. 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 };
});
Fastify позволяет использовать JSON Schema, однако Zod можно адаптировать через преобразование.
import { z } from "zod";
function zodToFastifySchema(schema: z.ZodSchema) {
return schema; // упрощённый адаптер
}
В реальных системах используется более строгая трансляция через промежуточные библиотеки.
Плагин может централизовать поведение валидации:
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;
});
В крупных приложениях схемы организуются по слоям:
Zod используется как трансформатор между слоями:
const apiSchema = z.object({
price: z.string(),
});
const domainSchema = apiSchema.transform((data) => ({
price: Number(data.price),
}));
Zod поддерживает преобразования через transform, что
позволяет использовать middleware не только для проверки, но и для
нормализации данных.
const schema = z.object({
page: z.string().transform(Number),
lim it: z.string().transform(v => Math.min(Number(v), 100)),
});
После валидации данные уже находятся в нужном формате, исключая дополнительную обработку в контроллерах.
Гибкость 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.
При увеличении числа эндпоинтов становится критичным единый подход к:
Zod в связке с middleware решает задачу унификации контрактов между слоями приложения без необходимости введения отдельного DSL или генераторов схем.