Схемы для REST endpoints

При проектировании REST API ключевым элементом становится строгая типизация входных и выходных данных. В экосистеме JavaScript одним из наиболее выразительных инструментов описания таких контрактов выступает Zod, позволяющий формализовать структуру запросов и ответов в виде исполняемых схем валидации.

REST endpoint в прикладной архитектуре обычно разделяется на несколько независимых частей данных:

  • params — параметры пути (path parameters)
  • query — строка запроса
  • body — тело запроса
  • response — структура ответа

Каждая из этих частей требует собственной схемы, поскольку различается семантика, источники данных и требования к валидации.


Базовые принципы схем в Zod для API-контрактов

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-слоя.


Схема параметров пути (params)

Path parameters в REST обычно представляют идентификаторы ресурсов.

const UserParamsSchema = z.object({
  userId: z.string().uuid(),
});

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


Схема query-параметров

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)

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,
});

Объединение схем в контракт endpoint

Полная спецификация 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();
}

Использование схем в Express

Интеграция с 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

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"),
});

Дискриминированные объединения для endpoint-поведения

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(),
});

Версионирование схем REST API

При изменении контрактов сохраняется совместимость через расширение:

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

const UserV2Schema = UserV1Schema.extend({
  email: z.string().email(),
});

Такой подход позволяет эволюционно развивать API без разрушения старых клиентов.


Интеграция схем с типизацией TypeScript

Zod автоматически выводит типы:

type CreateUserBody = z.infer<typeof CreateUserBodySchema>;

Это устраняет дублирование между runtime-валидацией и compile-time типами.


Безопасность данных на уровне схем

Схемы выполняют роль первого слоя защиты:

  • ограничение глубины объектов
  • контроль длины строк
  • валидация форматов UUID, email, URL
  • исключение лишних полей через .strict()
const StrictUserSchema = z.object({
  email: z.string().email(),
}).strict();

Инкапсуляция REST-контрактов

Типичная структура API-модуля организуется через группировку схем:

const UserEndpoint = {
  params: UserParamsSchema,
  query: UserQuerySchema,
  body: CreateUserBodySchema,
  response: UserSchema,
};

Такая модель позволяет рассматривать endpoint как формальный контракт, а не как набор разрозненных проверок.