Validation
Runtime response validation using Standard Schema in @zap-studio/fetch.
@zap-studio/fetch uses Standard Schema for runtime validation, which means it works with any schema library that implements the Standard Schema specification.
Why Validation Matters
APIs change. Without runtime validation, you might get data that doesn't match your TypeScript types, causing subtle bugs that are hard to track down:
// Without validation
const user = await fetch("/api/users/1").then((r) => r.json()) as User;
// What if the API returns { id: "123" } instead of { id: 123 }?
// TypeScript thinks id is a number, but it's actually a string!
user.id + 1; // "1231" instead of 124 😱With @zap-studio/fetch, you get runtime validation that catches these issues immediately:
// With validation
const user = await api.get("/api/users/1", UserSchema);
// If the API returns { id: "123" }, you get a ValidationError
// instead of silent type mismatchSupported Schema Libraries
Any library implementing Standard Schema v1 is supported:
- Zod — The most popular TypeScript-first schema library
- Valibot — Smaller bundle size alternative
- ArkType — 1:1 TypeScript syntax
- TypeBox — JSON Schema compatible
- And more...
Using Different Schema Libraries
Zod
import { z } from "zod";
import { api } from "@zap-studio/fetch";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
createdAt: z.string().datetime(),
});
const user = await api.get("/users/1", UserSchema);Valibot
import * as v from "valibot";
import { api } from "@zap-studio/fetch";
const UserSchema = v.object({
id: v.number(),
name: v.string(),
email: v.pipe(v.string(), v.email()),
createdAt: v.pipe(v.string(), v.isoTimestamp()),
});
const user = await api.get("/users/1", UserSchema);ArkType
import { type } from "arktype";
import { api } from "@zap-studio/fetch";
const UserSchema = type({
id: "number",
name: "string",
email: "email",
createdAt: "string",
});
const user = await api.get("/users/1", UserSchema);The standardValidate Helper
For standalone validation needs, use the standardValidate helper:
import { standardValidate } from "@zap-studio/fetch/validator";
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
});
// Throwing validation (default)
const user = await standardValidate(UserSchema, data, true);
// Returns validated data or throws ValidationError
// Non-throwing validation
const result = await standardValidate(UserSchema, data, false);
// Returns { value: T } or { issues: Issue[] }
if (result.issues) {
console.error("Validation failed:", result.issues);
} else {
console.log("Valid user:", result.value);
}isStandardSchema Type Guard
Check if a value is a Standard Schema:
import { isStandardSchema } from "@zap-studio/fetch/validator";
const schema = z.object({ name: z.string() });
if (isStandardSchema(schema)) {
// TypeScript knows schema is StandardSchemaV1
const result = schema["~standard"].validate(data);
}Validation Flow
When you pass a schema to $fetch or api.* methods:
- The HTTP request is made
- The response JSON is parsed
- The data is validated against your schema
- If valid, the typed data is returned
- If invalid and
throwOnValidationError: true, aValidationErroris thrown - If invalid and
throwOnValidationError: false, a Result object is returned
// Simplified internal flow
const response = await fetch(url, options);
const rawData = await response.json();
const validatedData = await standardValidate(schema, rawData, throwOnValidationError);
return validatedData;Handling Validation Results
Throwing Mode (Default)
import { ValidationError } from "@zap-studio/fetch/errors";
try {
const user = await api.get("/users/1", UserSchema);
// user is fully typed: { id: number; name: string; email: string; }
} catch (error) {
if (error instanceof ValidationError) {
for (const issue of error.issues) {
console.error(`${issue.path?.join(".")}: ${issue.message}`);
}
}
}Non-Throwing Mode
const result = await api.get("/users/1", UserSchema, {
throwOnValidationError: false,
});
if (result.issues) {
result.issues.forEach((issue) => {
console.error(`${issue.path?.join(".")}: ${issue.message}`);
});
} else {
console.log("User name:", result.value.name);
}Type Inference
Types are automatically inferred from your schema:
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
roles: z.array(z.enum(["admin", "user"])),
});
const user = await api.get("/users/1", UserSchema);
// TypeScript infers:
// {
// id: number;
// name: string;
// email: string;
// roles: ("admin" | "user")[];
// }Best Practices
1. Define Schemas Once, Reuse Everywhere
// schemas/user.ts
import { z } from "zod";
export const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
createdAt: z.string().transform((s) => new Date(s)),
});
export type User = z.infer<typeof UserSchema>;
// api/users.ts
import { api } from "@zap-studio/fetch";
import { UserSchema, type User } from "@/schemas/user";
export async function getUser(id: number): Promise<User> {
return api.get(`/users/${id}`, UserSchema);
}2. Use Strict Schemas
Validate exactly what you expect:
// Too loose - accepts any extra fields
const LooseSchema = z.object({ id: z.number() });
// Better - rejects unknown fields
const StrictSchema = z.object({ id: z.number() }).strict();3. Handle Validation Errors Gracefully
APIs can change unexpectedly:
async function getUser(id: string) {
try {
return await api.get(`/users/${id}`, UserSchema);
} catch (error) {
if (error instanceof ValidationError) {
// Log for debugging but don't crash
console.error("API response changed:", error.issues);
return null;
}
throw error;
}
}4. Use Schema Transforms
Parse and transform data in your schema:
const DateSchema = z.string().transform((s) => new Date(s));
const PriceSchema = z.number().transform((cents) => ({
cents,
dollars: cents / 100,
formatted: `$${(cents / 100).toFixed(2)}`,
}));5. Compose Schemas
Build complex schemas from simpler parts:
const AddressSchema = z.object({
street: z.string(),
city: z.string(),
country: z.string(),
});
const UserSchema = z.object({
id: z.string(),
name: z.string(),
address: AddressSchema.optional(),
});
const OrderSchema = z.object({
id: z.string(),
user: UserSchema,
shippingAddress: AddressSchema,
billingAddress: AddressSchema.optional(),
});Last updated on