При проектировании REST API ключевым элементом становится строгая типизация входных и выходных данных. В экосистеме JavaScript одним из наиболее выразительных инструментов описания таких контрактов выступает Zod, позволяющий формализовать структуру запросов и ответов в виде исполняемых схем валидации.
REST endpoint в прикладной архитектуре обычно разделяется на несколько независимых частей данных:
Каждая из этих частей требует собственной схемы, поскольку различается семантика, источники данных и требования к валидации.
Zod предоставляет декларативный способ описания структуры данных через композицию примитивов и операторов.
Простейшие схемы:
import { z } fr om "zod";
const IdSchema = z.string().uuid();
const PaginationSchema = z.object({
page: z.coerce.number().int().min(1),
lim it: z.coerce.number().int().min(1).max(100),
});
Ключевым моментом становится использование z.coerce,
позволяющего преобразовывать строковые значения query-параметров в
числовые, что критично для HTTP-слоя.
Path parameters в REST обычно представляют идентификаторы ресурсов.
const UserParamsSchema = z.object({
userId: z.string().uuid(),
});
При использовании в маршрутизации такие схемы обеспечивают строгую проверку идентификаторов до попадания в бизнес-логику.
Query string используется для фильтрации, сортировки и пагинации.
const UserQuerySchema = z.object({
search: z.string().optional(),
sort: z.enum(["asc", "desc"]).optional(),
page: z.coerce.number().int().min(1).optional(),
limit: z.coerce.number().int().min(1).max(100).optional(),
});
Композиция схем позволяет переиспользовать общие блоки:
const BaseQuerySchema = z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
});
Body-запросы содержат бизнес-данные и требуют наиболее строгой структуры.
const CreateUserBodySchema = z.object({
email: z.string().email(),
password: z.string().min(8),
name: z.string().min(1),
});
Для сложных объектов применяется вложенная структура:
const AddressSchema = z.object({
country: z.string(),
city: z.string(),
zip: z.string(),
});
const CreateProfileBodySchema = z.object({
name: z.string(),
address: AddressSchema,
});
Полная спецификация REST endpoint формируется через композицию частей:
const CreateUserEndpointSchema = {
params: z.object({}),
query: z.object({}),
body: CreateUserBodySchema,
response: z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string(),
}),
};
Такое разделение позволяет явно фиксировать контракт между слоями системы.
Для серверной логики используется единая точка валидации:
function validateCreateUser(input) {
return CreateUserBodySchema.parse(input);
}
Метод parse обеспечивает строгую проверку, тогда как
safeParse позволяет избежать исключений:
const result = CreateUserBodySchema.safeParse(input);
if (!result.success) {
const errors = result.error.format();
}
Интеграция с HTTP-слоем:
app.post("/users/:userId", (req, res) => {
const params = UserParamsSchema.parse(req.params);
const body = CreateUserBodySchema.parse(req.body);
res.json({
id: params.userId,
...body,
});
});
Разделение params, query и
body предотвращает смешивание уровней данных.
Fastify поддерживает декларативную валидацию через JSON Schema, но Zod может использоваться на уровне бизнес-логики:
fastify.post("/users/:userId", async (req) => {
const params = UserParamsSchema.parse(req.params);
const body = CreateUserBodySchema.parse(req.body);
return {
id: params.userId,
...body,
};
});
Ключевая особенность архитектуры Zod-схем — композиция:
const TimestampSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
});
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
}).merge(TimestampSchema);
Также применяется расширение:
const AdminUserSchema = UserSchema.extend({
role: z.literal("admin"),
});
REST API часто требует различных форматов ответа:
const SuccessResponse = z.object({
status: z.literal("success"),
data: z.object({
id: z.string(),
}),
});
const ErrorResponse = z.object({
status: z.literal("error"),
message: z.string(),
});
const ResponseSchema = z.discriminatedUnion("status", [
SuccessResponse,
ErrorResponse,
]);
Такая модель позволяет формализовать контракт ошибок на уровне типов.
Zod поддерживает преобразования данных на этапе парсинга:
const NumberIdSchema = z.string().transform((val) => Number(val));
Или нормализацию входных данных:
const TrimmedStringSchema = z.string().transform((s) => s.trim());
Это особенно полезно для query-параметров и внешних API.
Типовая структура API часто повторяется:
const SortSchema = z.object({
sortBy: z.string(),
order: z.enum(["asc", "desc"]).default("asc"),
});
const ListQuerySchema = BaseQuerySchema.merge(SortSchema);
Фильтры формализуются через вложенные структуры:
const UserFilterSchema = z.object({
role: z.enum(["user", "admin"]).optional(),
active: z.boolean().optional(),
});
При изменении контрактов сохраняется совместимость через расширение:
const UserV1Schema = z.object({
id: z.string(),
name: z.string(),
});
const UserV2Schema = UserV1Schema.extend({
email: z.string().email(),
});
Такой подход позволяет эволюционно развивать API без разрушения старых клиентов.
Zod автоматически выводит типы:
type CreateUserBody = z.infer<typeof CreateUserBodySchema>;
Это устраняет дублирование между runtime-валидацией и compile-time типами.
Схемы выполняют роль первого слоя защиты:
.strict()const StrictUserSchema = z.object({
email: z.string().email(),
}).strict();
Типичная структура API-модуля организуется через группировку схем:
const UserEndpoint = {
params: UserParamsSchema,
query: UserQuerySchema,
body: CreateUserBodySchema,
response: UserSchema,
};
Такая модель позволяет рассматривать endpoint как формальный контракт, а не как набор разрозненных проверок.