DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Zod Schema Validation: Runtime Type Safety for TypeScript APIs

Zod Schema Validation: Runtime Type Safety for TypeScript APIs

TypeScript types disappear at runtime. A user can send any JSON payload — Zod validates it at runtime and infers TypeScript types from the same schema.

Why Zod

// Without Zod — you're trusting the user
app.post('/users', (req, res) => {
  const { name, email } = req.body; // Could be anything
  await db.user.create({ data: { name, email } }); // SQL injection risk
});

// With Zod — validated and typed
const CreateUserSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
});

app.post('/users', (req, res) => {
  const result = CreateUserSchema.safeParse(req.body);
  if (!result.success) return res.status(400).json(result.error.flatten());
  await db.user.create({ data: result.data }); // result.data is typed
});
Enter fullscreen mode Exit fullscreen mode

Core Schemas

import { z } from 'zod';

// Primitives
z.string()          // any string
z.string().email()  // valid email
z.string().url()    // valid URL
z.string().uuid()   // valid UUID
z.number().int()    // integer
z.number().min(0)   // non-negative
z.boolean()
z.date()

// Objects
const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string(),
  email: z.string().email(),
  age: z.number().int().min(0).max(150).optional(),
  role: z.enum(['admin', 'user', 'guest']),
  createdAt: z.coerce.date(), // converts string to Date
});

// Infer TypeScript type from schema
type User = z.infer<typeof UserSchema>;
Enter fullscreen mode Exit fullscreen mode

Nested Validation

const OrderSchema = z.object({
  id: z.string(),
  items: z.array(z.object({
    productId: z.string(),
    quantity: z.number().int().positive(),
    price: z.number().positive(),
  })).min(1, 'Order must have at least one item'),
  shipping: z.object({
    address: z.string(),
    city: z.string(),
    country: z.string().length(2), // ISO country code
  }),
  discount: z.number().min(0).max(100).optional(),
});
Enter fullscreen mode Exit fullscreen mode

Custom Validators

const PasswordSchema = z.string()
  .min(8)
  .regex(/[A-Z]/, 'Must contain uppercase')
  .regex(/[0-9]/, 'Must contain number')
  .regex(/[^A-Za-z0-9]/, 'Must contain special character');

const DateRangeSchema = z.object({
  start: z.coerce.date(),
  end: z.coerce.date(),
}).refine(data => data.end > data.start, {
  message: 'End date must be after start date',
  path: ['end'],
});
Enter fullscreen mode Exit fullscreen mode

Transforming Data

const SlugSchema = z.string()
  .transform(s => s.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''));

const TagsSchema = z.union([
  z.string().transform(s => s.split(',').map(t => t.trim())),
  z.array(z.string()),
]);
Enter fullscreen mode Exit fullscreen mode

Error Formatting

const result = UserSchema.safeParse(input);
if (!result.success) {
  // Flat format for simple forms
  const { fieldErrors } = result.error.flatten();
  // { email: ['Invalid email'], name: ['Required'] }

  // Full format with paths
  const issues = result.error.issues;
  // [{ path: ['email'], message: 'Invalid email', code: 'invalid_string' }]
}
Enter fullscreen mode Exit fullscreen mode

React Hook Form Integration

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';

const { register, handleSubmit, formState: { errors } } = useForm({
  resolver: zodResolver(CreateUserSchema),
});
// Validation runs on submit — errors populated automatically
Enter fullscreen mode Exit fullscreen mode

Zod ships pre-integrated in the AI SaaS Starter Kit — all API routes validated, forms use zodResolver, shared types between client/server. $99 at whoffagents.com.

Top comments (0)