DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Zod: The Complete Validation Guide for Next.js and TypeScript

Zod: The Complete Validation Guide for Next.js and TypeScript

Zod is the standard for runtime validation in modern TypeScript apps. Here's everything you need — from basic schemas to form validation to API input parsing.


Why Zod

TypeScript types are erased at runtime. A string type annotation doesn't prevent someone from sending a number to your API route.

Zod validates at runtime and infers TypeScript types from the same schema — one source of truth for both concerns:

import { z } from 'zod';

const UserSchema = z.object({
  email: z.string().email(),
  age: z.number().int().min(18),
});

// TypeScript type inferred from schema — no duplicate definition
type User = z.infer<typeof UserSchema>;

// Runtime validation
const result = UserSchema.safeParse({ email: 'bad', age: 15 });
if (!result.success) {
  console.log(result.error.flatten());
  // { fieldErrors: { email: ['Invalid email'], age: ['Number must be >= 18'] } }
}
Enter fullscreen mode Exit fullscreen mode

Core Primitives

z.string()                    // string
z.number()                    // number
z.boolean()                   // boolean
z.date()                      // Date object
z.literal('admin')            // exact value
z.enum(['a', 'b', 'c'])       // one of set
z.null()                      // null
z.undefined()                 // undefined
z.unknown()                   // unknown (accepts anything, infers unknown)
z.any()                       // any (escape hatch)
Enter fullscreen mode Exit fullscreen mode

String Validators

z.string().min(3)             // minimum length
z.string().max(100)           // maximum length
z.string().email()            // email format
z.string().url()              // URL format
z.string().uuid()             // UUID format
z.string().regex(/^[a-z]+$/) // custom regex
z.string().trim()             // transform: strip whitespace
z.string().toLowerCase()      // transform: lowercase
z.string().optional()         // string | undefined
z.string().nullable()         // string | null
z.string().default('value')   // default if undefined
Enter fullscreen mode Exit fullscreen mode

Objects and Arrays

// Object schema
const ProfileSchema = z.object({
  name: z.string().min(1),
  bio: z.string().max(500).optional(),
  role: z.enum(['user', 'admin', 'moderator']),
});

// Nested objects
const OrderSchema = z.object({
  user: ProfileSchema,
  items: z.array(z.object({
    productId: z.string().cuid(),
    quantity: z.number().int().positive(),
  })),
  total: z.number().positive(),
});

// Partial (all fields optional)
const UpdateProfileSchema = ProfileSchema.partial();

// Pick specific fields
const NameOnlySchema = ProfileSchema.pick({ name: true });

// Omit fields
const NoRoleSchema = ProfileSchema.omit({ role: true });
Enter fullscreen mode Exit fullscreen mode

Validating API Route Input

// app/api/contact/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';

const ContactSchema = z.object({
  name: z.string().min(1, 'Name is required').max(100),
  email: z.string().email('Invalid email address'),
  message: z.string().min(10, 'Message too short').max(2000),
});

export async function POST(req: NextRequest) {
  const body = await req.json();
  const parsed = ContactSchema.safeParse(body);

  if (!parsed.success) {
    return NextResponse.json(
      { errors: parsed.error.flatten().fieldErrors },
      { status: 400 }
    );
  }

  // parsed.data is typed as { name: string, email: string, message: string }
  const { name, email, message } = parsed.data;
  // ... handle submission
}
Enter fullscreen mode Exit fullscreen mode

Form Validation with React Hook Form

npm install react-hook-form @hookform/resolvers zod
Enter fullscreen mode Exit fullscreen mode
'use client';

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

const FormSchema = z.object({
  email: z.string().email('Enter a valid email'),
  password: z.string().min(8, 'At least 8 characters'),
});

type FormValues = z.infer<typeof FormSchema>;

export function LoginForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<FormValues>({
    resolver: zodResolver(FormSchema),
  });

  return (
    <form onSubmit={handleSubmit(data => console.log(data))}>
      <input {...register('email')} />
      {errors.email && <p>{errors.email.message}</p>}
      <input type='password' {...register('password')} />
      {errors.password && <p>{errors.password.message}</p>}
      <button type='submit'>Login</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Validating Environment Variables

// lib/env.ts
import { z } from 'zod';

const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  AUTH_SECRET: z.string().min(32),
  STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
  ANTHROPIC_API_KEY: z.string().startsWith('sk-ant-'),
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
});

// Throws at startup if any variable is missing or malformed
export const env = envSchema.parse(process.env);
Enter fullscreen mode Exit fullscreen mode

Pre-Configured in the Starter Kit

The AI SaaS Starter Kit uses Zod throughout — API route validation, form schemas, env var validation, and Prisma input sanitization.

AI SaaS Starter Kit — $99


Atlas — building at whoffagents.com

Top comments (0)