Claude Code for Zod: Runtime Validation and Type-Safe Schemas — Claude Skills 360 Blog
Blog / Frontend / Claude Code for Zod: Runtime Validation and Type-Safe Schemas
Frontend

Claude Code for Zod: Runtime Validation and Type-Safe Schemas

Published: October 31, 2026
Read time: 7 min read
By: Claude Skills 360

Zod bridges TypeScript’s compile-time types and JavaScript’s runtime. TypeScript types disappear at runtime — API responses can contain anything, form inputs are always strings, environment variables are unknown. Zod defines schemas that validate data at runtime AND infer TypeScript types from the schema, so you write the shape once. Claude Code writes Zod schemas for API requests, forms, environment configs, and the error handling that surfaces validation failures clearly.

CLAUDE.md for Zod Projects

## Validation Stack
- Zod 3.x for all runtime validation
- z.infer<typeof schema> for TypeScript types — no separate type definitions
- API boundaries: validate all incoming requests at the edge (middleware or route handler)
- Environment: z.object().parse(process.env) at startup — fail fast on missing vars
- Forms: React Hook Form with zodResolver — schema drives both validation and types
- Error handling: ZodError.flatten() for user-facing messages; z.safeParse() at boundaries

Core Schema Patterns

// schemas/order.ts — define schema, infer type from it
import { z } from 'zod';

// Reusable primitives
const PositiveInt = z.number().int().positive();
const NonEmptyString = z.string().min(1).max(255).trim();
const EmailSchema = z.string().email().toLowerCase();
const UUIDSchema = z.string().uuid();

// Enums — single source of truth for valid values
const OrderStatusSchema = z.enum([
  'pending', 'processing', 'shipped', 'delivered', 'cancelled'
]);

const OrderItemSchema = z.object({
  productId: UUIDSchema,
  quantity: PositiveInt.max(100),
  unitPriceCents: PositiveInt,
  productName: NonEmptyString,
});

export const CreateOrderSchema = z.object({
  customerId: UUIDSchema,
  items: z.array(OrderItemSchema).min(1, 'At least one item required').max(50),
  shippingAddress: z.object({
    street: NonEmptyString,
    city: NonEmptyString,
    country: z.string().length(2).toUpperCase(),
    postalCode: z.string().min(3).max(10).regex(/^[A-Z0-9\s-]+$/i),
  }),
  currency: z.string().length(3).toUpperCase().default('USD'),
  notes: z.string().max(500).optional(),
});

// Type inferred from schema — no duplication
export type CreateOrderInput = z.infer<typeof CreateOrderSchema>;

// Response schema — validate what the API returns
export const OrderSchema = CreateOrderSchema.extend({
  id: UUIDSchema,
  status: OrderStatusSchema,
  totalCents: PositiveInt,
  createdAt: z.coerce.date(),  // coerce: accepts ISO string or Date
  updatedAt: z.coerce.date(),
});

export type Order = z.infer<typeof OrderSchema>;

API Request Validation (Express/Fastify)

// middleware/validate.ts — Express middleware for request validation
import { Request, Response, NextFunction } from 'express';
import { ZodSchema, ZodError } from 'zod';

export function validateBody<T>(schema: ZodSchema<T>) {
  return (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse(req.body);
    
    if (!result.success) {
      // Flatten to field-level errors for API clients
      const errors = result.error.flatten().fieldErrors;
      return res.status(400).json({
        error: 'Validation failed',
        fields: errors,
      });
    }
    
    // Replace body with parsed (transformed) value
    req.body = result.data;
    next();
  };
}

// Usage in route:
// router.post('/orders', validateBody(CreateOrderSchema), createOrderHandler);

// For query params:
export function validateQuery<T>(schema: ZodSchema<T>) {
  return (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse(req.query);
    if (!result.success) {
      return res.status(400).json({ error: 'Invalid query parameters' });
    }
    req.query = result.data as any;
    next();
  };
}

const PaginationSchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  pageSize: z.coerce.number().int().min(1).max(100).default(20),
  status: OrderStatusSchema.optional(),
});

Environment Variable Validation

// config/env.ts — fail fast on startup if env is misconfigured
import { z } from 'zod';

const EnvSchema = z.object({
  NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
  PORT: z.coerce.number().int().min(1000).max(65535).default(3000),
  DATABASE_URL: z.string().url().startsWith('postgresql://'),
  JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 chars'),
  STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
  REDIS_URL: z.string().url().startsWith('redis://').optional(),
  LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
});

// Parse at module load — throws immediately if invalid
export const env = EnvSchema.parse(process.env);

// TypeScript now knows env.PORT is a number, env.NODE_ENV is a union type, etc.

React Hook Form + Zod

// components/CreateOrderForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { CreateOrderSchema, type CreateOrderInput } from '../schemas/order';

export function CreateOrderForm({ onSubmit }: { onSubmit: (data: CreateOrderInput) => void }) {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
    setError,
  } = useForm<CreateOrderInput>({
    resolver: zodResolver(CreateOrderSchema),
    defaultValues: {
      currency: 'USD',
      items: [{ productId: '', quantity: 1, unitPriceCents: 0, productName: '' }],
    },
  });
  
  const submit = async (data: CreateOrderInput) => {
    try {
      await onSubmit(data);
    } catch (err) {
      // Map server validation errors back to form fields
      if (err.response?.data?.fields) {
        for (const [field, messages] of Object.entries(err.response.data.fields)) {
          setError(field as any, { message: (messages as string[])[0] });
        }
      }
    }
  };
  
  return (
    <form onSubmit={handleSubmit(submit)}>
      <div>
        <label htmlFor="customerId">Customer ID</label>
        <input id="customerId" {...register('customerId')} />
        {errors.customerId && <span className="error">{errors.customerId.message}</span>}
      </div>
      
      <div>
        <label htmlFor="street">Street</label>
        <input id="street" {...register('shippingAddress.street')} />
        {errors.shippingAddress?.street && (
          <span className="error">{errors.shippingAddress.street.message}</span>
        )}
      </div>
      
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Placing...' : 'Place Order'}
      </button>
    </form>
  );
}

Custom Refinements and Transforms

// schemas/advanced.ts — complex validation with refinements
import { z } from 'zod';

// Refine: add cross-field validation
const DateRangeSchema = z.object({
  startDate: z.coerce.date(),
  endDate: z.coerce.date(),
}).refine(
  (data) => data.endDate > data.startDate,
  {
    message: 'End date must be after start date',
    path: ['endDate'],  // Error shows on this field
  }
);

// Transform: shape data on parse
const MoneySchema = z.object({
  amount: z.string().regex(/^\d+(\.\d{1,2})?$/),
  currency: z.string().length(3),
}).transform((val) => ({
  ...val,
  amountCents: Math.round(parseFloat(val.amount) * 100),
  currency: val.currency.toUpperCase(),
}));

// z.discriminatedUnion: efficient type narrowing
const EventSchema = z.discriminatedUnion('type', [
  z.object({ type: z.literal('order.created'), orderId: z.string() }),
  z.object({ type: z.literal('payment.completed'), chargeId: z.string(), amount: z.number() }),
  z.object({ type: z.literal('order.cancelled'), orderId: z.string(), reason: z.string() }),
]);

type Event = z.infer<typeof EventSchema>;
// TypeScript narrows correctly after discriminating on type field

For the tRPC integration that uses Zod schemas for end-to-end type safety, the tRPC guide shows how Zod input schemas drive both router validation and the TypeScript client. For the React form patterns built on top of this validation layer, the React frontend guide covers controlled/uncontrolled components with validation UX. The Claude Skills 360 bundle includes Zod skill sets covering schema design, env validation, form integration, and API boundary patterns. Start with the free tier to try Zod schema generation.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free