Одной из ключевых проблем при работе с внешними API в JavaScript остаётся несоответствие между ожидаемой и фактической структурой данных. Даже при наличии TypeScript типы существуют только на этапе компиляции, тогда как данные приходят в рантайме и могут быть некорректными, неполными или изменёнными без предупреждения. Решение этой проблемы строится вокруг идеи runtime-валидации + статической типизации, где центральную роль занимает Zod.
Типобезопасный API-клиент начинается не с HTTP-запроса, а с описания контракта данных. Контракт определяет:
С использованием Zod контракт становится исполняемым кодом, а не только TypeScript-интерфейсом.
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
isActive: z.boolean()
});
Такой подход позволяет использовать одну и ту же схему для:
Одним из фундаментальных преимуществ Zod является возможность автоматического получения TypeScript-типа:
type User = z.infer<typeof UserSchema>;
Это устраняет дублирование описания модели данных и снижает вероятность рассинхронизации типов и рантайм-логики.
Типовой API-клиент строится вокруг трёх этапов:
async function fetchUser(id: number) {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return UserSchema.parse(data);
}
В этом случае любые несоответствия структуры данных приводят не к скрытым багам, а к явной ошибке валидации.
Жёсткая валидация через parse может быть заменена на
мягкую через safeParse, что особенно важно в клиентских
приложениях:
const result = UserSchema.safeParse(data);
if (!result.success) {
console.error(result.error);
return null;
}
const user = result.data;
Структура result всегда детерминирована:
success: true → доступ к datasuccess: false → доступ к errorТакой подход позволяет строить устойчивые API-клиенты без исключений в потоке выполнения.
Типобезопасный API-клиент обычно инкапсулирует работу с HTTP в отдельный слой:
async function apiRequest<T>(url: string, schema: z.ZodType<T>): Promise<T> {
const res = await fetch(url);
const json = await res.json();
return schema.parse(json);
}
Использование:
const user = await apiRequest("/api/user/1", UserSchema);
Такой паттерн обеспечивает:
Реальные API редко возвращают плоские структуры. Zod позволяет композиционно описывать сложные ответы:
const PostSchema = z.object({
id: z.number(),
title: z.string(),
author: UserSchema,
tags: z.array(z.string())
});
Композиция схем обеспечивает:
Типичный ответ API с пагинацией:
const PaginatedUsersSchema = z.object({
items: z.array(UserSchema),
total: z.number(),
page: z.number(),
pageSize: z.number()
});
Использование:
const result = PaginatedUsersSchema.parse(data);
Такой подход позволяет стандартизировать обработку списков сущностей.
Zod позволяет не только проверять данные, но и преобразовывать их:
const UserSchema = z.object({
id: z.string().transform(val => Number(val)),
name: z.string()
});
Это особенно полезно при работе с API, где типы не всегда согласованы (например, ID как строка вместо числа).
Часто API возвращает данные в неудобной форме. Zod позволяет выполнять нормализацию:
const ApiUserSchema = z.object({
user_id: z.number(),
user_name: z.string()
});
const UserSchema = ApiUserSchema.transform(data => ({
id: data.user_id,
name: data.user_name
}));
Таким образом формируется внутренний доменный формат данных, независимый от внешнего API.
На уровне архитектуры API-клиент может быть структурирован по ресурсам:
const UserApi = {
getById: (id: number) =>
apiRequest(`/api/users/${id}`, UserSchema),
list: () =>
apiRequest(`/api/users`, PaginatedUsersSchema)
};
Такой подход:
Типобезопасность должна распространяться не только на ответы, но и на запросы:
const GetUserParams = z.object({
id: z.number().positive()
});
function getUser(params: unknown) {
const valid = GetUserParams.parse(params);
return fetch(`/api/users/${valid.id}`);
}
Это защищает API-клиент от некорректных вызовов ещё до выполнения запроса.
Работа с URL-параметрами часто становится источником ошибок:
const QuerySchema = z.object({
search: z.string().optional(),
page: z.number().default(1)
});
function buildQuery(params: z.infer<typeof QuerySchema>) {
const valid = QuerySchema.parse(params);
return new URLSearchParams({
page: String(valid.page),
search: valid.search ?? ""
});
}
В реальных условиях API может возвращать разные структуры в зависимости от версии или состояния системы. Zod позволяет описывать альтернативные варианты:
const ResponseSchema = z.union([
z.object({ status: z.literal("ok"), data: UserSchema }),
z.object({ status: z.literal("error"), message: z.string() })
]);
Это позволяет явно моделировать неопределённость API.
При изменении API важно сохранять обратную совместимость:
const UserV1 = z.object({
id: z.number(),
name: z.string()
});
const UserV2 = UserV1.extend({
email: z.string().email()
});
Такой подход позволяет постепенно эволюционировать контракт данных без нарушения существующих клиентов.
Типобезопасный клиент часто оборачивает fetch:
async function safeFetch<T>(
url: string,
schema: z.ZodType<T>
): Promise<T> {
const res = await fetch(url);
if (!res.ok) {
throw new Error("HTTP error");
}
const json = await res.json();
return schema.parse(json);
}
Ключевая архитектурная идея заключается в разделении:
Zod выступает как слой адаптации между ними, обеспечивая строгую границу и предсказуемость данных в приложении.
При работе с сложными API часто встречаются вложенные объекты:
const CommentSchema = z.object({
id: z.number(),
text: z.string(),
author: UserSchema,
replies: z.array(z.lazy(() => CommentSchema))
});
Рекурсивные схемы позволяют описывать древовидные структуры без потери типобезопасности.
Zod позволяет выражать бизнес-правила прямо в схеме:
const AgeSchema = z.number().min(18).max(120);
const PasswordSchema = z.string().min(8);
Это переносит часть логики из бизнес-слоя в слой данных, снижая дублирование проверок.
Ошибки Zod имеют структурированный формат:
Это позволяет строить унифицированные системы обработки ошибок API-клиентов, включая:
Типобезопасный API-клиент на базе Zod формируется как совокупность слоёв:
Такая структура обеспечивает детерминированное поведение системы при работе с внешними сервисами и минимизирует расхождения между ожидаемыми и фактическими данными.