Zod Validation in Next.js: Complete Guide from Beginner to Expert
Zod Validation in Next.js: Complete Guide from Beginner to Expert
Zod validation in Next.js provides type-safe validation for API routes, Server Actions, forms, and middleware. This comprehensive guide covers everything from basic setup to expert-level patterns for production applications.
Why Zod in Next.js?
Next.js applications benefit from Zod validation because:
Installation
1npm install zod2# For form validation3npm install react-hook-form @hookform/resolvers4# For API validation utilities5npm install zod-form-dataAPI Route Validation
Basic API Route Validation
1// app/api/users/route.ts2import { NextRequest, NextResponse } from 'next/server';3import { z } from 'zod';4 5const createUserSchema = z.object({6 name: z.string().min(1, 'Name is required'),7 email: z.string().email('Invalid email'),8 age: z.number().int().min(18, 'Must be at least 18')9});10 11export async function POST(request: NextRequest) {12 try {13 const body = await request.json();14 15 // Validate request bodyAdvanced API Route Validation
1// app/api/users/[id]/route.ts2import { NextRequest, NextResponse } from 'next/server';3import { z } from 'zod';4 5// Query parameter validation6const getUserParamsSchema = z.object({7 id: z.string().uuid('Invalid user ID')8});9 10// Query string validation11const getUserQuerySchema = z.object({12 include: z.enum(['posts', 'comments', 'all']).optional(),13 page: z.string().transform(Number).pipe(z.number().int().positive()).optional(),14 limit: z.string().transform(Number).pipe(z.number().int().positive().max(100)).optional()15});Form Data Validation
1// app/api/upload/route.ts2import { NextRequest, NextResponse } from 'next/server';3import { z } from 'zod';4 5const uploadSchema = z.object({6 title: z.string().min(1, 'Title is required'),7 description: z.string().max(500).optional(),8 category: z.enum(['image', 'video', 'document']),9 tags: z.array(z.string()).max(10).optional()10});11 12export async function POST(request: NextRequest) {13 try {14 const formData = await request.formData();15 Server Actions Validation
Basic Server Action
1// app/actions/user-actions.ts2'use server';3 4import { z } from 'zod';5import { revalidatePath } from 'next/cache';6 7const createUserSchema = z.object({8 name: z.string().min(1, 'Name is required'),9 email: z.string().email('Invalid email'),10 password: z.string().min(8, 'Password must be at least 8 characters')11});12 13export async function createUser(formData: FormData) {14 try {15 const rawData = {Advanced Server Action with Error Handling
1// app/actions/user-actions.ts2'use server';3 4import { z } from 'zod';5import { revalidatePath } from 'next/cache';6 7const updateUserSchema = z.object({8 id: z.string().uuid(),9 name: z.string().min(1).optional(),10 email: z.string().email().optional(),11 bio: z.string().max(500).optional(),12 website: z.string().url().optional().or(z.literal(''))13}).refine(14 (data) => Object.keys(data).length > 1, // At least one field besides id15 { message: 'At least one field must be provided' }Using Server Actions in Forms
1// app/components/user-form.tsx2'use client';3 4import { useFormState, useFormStatus } from 'react-dom';5import { updateUser, type ActionResult } from '@/app/actions/user-actions';6 7function SubmitButton() {8 const { pending } = useFormStatus();9 return (10 <button type="submit" disabled={pending}>11 {pending ? 'Updating...' : 'Update User'}12 </button>13 );14}15 Middleware Validation
Request Validation in Middleware
1// middleware.ts2import { NextResponse } from 'next/server';3import type { NextRequest } from 'next/server';4import { z } from 'zod';5 6const apiKeySchema = z.string().min(1);7 8export function middleware(request: NextRequest) {9 // Validate API key in headers10 if (request.nextUrl.pathname.startsWith('/api/protected')) {11 const apiKey = request.headers.get('x-api-key');12 13 try {14 apiKeySchema.parse(apiKey);15 } catch (error) {Form Validation with React Hook Form
Client-Side Form Validation
1// app/components/contact-form.tsx2'use client';3 4import { useForm } from 'react-hook-form';5import { zodResolver } from '@hookform/resolvers/zod';6import { z } from 'zod';7import { useState } from 'react';8 9const contactSchema = z.object({10 name: z.string().min(1, 'Name is required'),11 email: z.string().email('Invalid email'),12 subject: z.string().min(1, 'Subject is required'),13 message: z.string().min(10, 'Message must be at least 10 characters'),14 phone: z.string().regex(/^\+?[1-9]\d{1,14}$/, 'Invalid phone number').optional()15});Advanced Validation Patterns
Environment Variable Validation
1// lib/env.ts2import { z } from 'zod';3 4const envSchema = z.object({5 DATABASE_URL: z.string().url(),6 NEXTAUTH_SECRET: z.string().min(32),7 NEXTAUTH_URL: z.string().url(),8 SMTP_HOST: z.string().optional(),9 SMTP_PORT: z.string().transform(Number).pipe(z.number().int()).optional(),10 SMTP_USER: z.string().optional(),11 SMTP_PASSWORD: z.string().optional()12});13 14export const env = envSchema.parse({15 DATABASE_URL: process.env.DATABASE_URL,Pagination Schema
1// lib/schemas/pagination.ts2import { z } from 'zod';3 4export const paginationSchema = z.object({5 page: z.string().transform(Number).pipe(z.number().int().positive()).default('1'),6 limit: z.string().transform(Number).pipe(z.number().int().positive().max(100)).default('10'),7 sortBy: z.string().optional(),8 sortOrder: z.enum(['asc', 'desc']).default('desc')9});10 11export type PaginationParams = z.infer<typeof paginationSchema>;Search and Filter Schema
1// lib/schemas/search.ts2import { z } from 'zod';3 4export const searchSchema = z.object({5 query: z.string().min(1, 'Search query is required'),6 category: z.enum(['all', 'posts', 'users', 'comments']).optional(),7 dateFrom: z.string().date().optional(),8 dateTo: z.string().date().optional(),9 tags: z.array(z.string()).optional()10}).refine(11 (data) => {12 if (data.dateFrom && data.dateTo) {13 return new Date(data.dateTo) >= new Date(data.dateFrom);14 }15 return true;File Upload Validation
1// lib/schemas/upload.ts2import { z } from 'zod';3 4const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB5const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/webp'];6 7export const imageUploadSchema = z.object({8 file: z9 .instanceof(File)10 .refine((file) => file.size <= MAX_FILE_SIZE, {11 message: 'File size must be less than 5MB'12 })13 .refine((file) => ACCEPTED_IMAGE_TYPES.includes(file.type), {14 message: 'Only JPEG, PNG, and WebP images are allowed'15 }),Real-World Examples
Complete CRUD API with Validation
1// app/api/posts/route.ts2import { NextRequest, NextResponse } from 'next/server';3import { z } from 'zod';4 5const createPostSchema = z.object({6 title: z.string().min(1, 'Title is required').max(200),7 content: z.string().min(10, 'Content must be at least 10 characters'),8 published: z.boolean().default(false),9 tags: z.array(z.string()).max(10).optional(),10 categoryId: z.string().uuid().optional()11});12 13const updatePostSchema = createPostSchema.partial().refine(14 (data) => Object.keys(data).length > 0,15 { message: 'At least one field must be provided' }Authentication with Validation
1// app/api/auth/register/route.ts2import { NextRequest, NextResponse } from 'next/server';3import { z } from 'zod';4 5const registerSchema = z.object({6 name: z.string().min(1, 'Name is required'),7 email: z.string().email('Invalid email'),8 password: z9 .string()10 .min(8, 'Password must be at least 8 characters')11 .regex(/[A-Z]/, 'Password must contain at least one uppercase letter')12 .regex(/[a-z]/, 'Password must contain at least one lowercase letter')13 .regex(/[0-9]/, 'Password must contain at least one number'),14 confirmPassword: z.string()15}).refine(Best Practices
1. Centralized Schema Definitions
1// lib/schemas/user.ts2import { z } from 'zod';3 4export const userSchema = z.object({5 id: z.string().uuid(),6 name: z.string().min(1),7 email: z.string().email(),8 createdAt: z.date()9});10 11export const createUserSchema = userSchema.omit({ id: true, createdAt: true });12export const updateUserSchema = createUserSchema.partial();2. Reusable Validation Utilities
1// lib/utils/validation.ts2import { z } from 'zod';3import { NextResponse } from 'next/server';4 5export function validateRequest<T>(6 schema: z.ZodSchema<T>,7 data: unknown8): { success: true; data: T } | { success: false; response: NextResponse } {9 try {10 const validatedData = schema.parse(data);11 return { success: true, data: validatedData };12 } catch (error) {13 if (error instanceof z.ZodError) {14 return {15 success: false,3. Type-Safe API Responses
1// lib/types/api.ts2import { z } from 'zod';3 4export const apiResponseSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>5 z.object({6 success: z.boolean(),7 data: dataSchema.optional(),8 errors: z.array(z.object({9 field: z.string(),10 message: z.string()11 })).optional()12 });13 14export type ApiResponse<T> = z.infer<ReturnType<typeof apiResponseSchema<z.ZodType<T>>>>;Expert Patterns
Schema Composition and Reusability
1// Base schemas2const baseEntitySchema = z.object({3 id: z.string().uuid(),4 createdAt: z.date(),5 updatedAt: z.date()6});7 8const timestampsSchema = z.object({9 createdAt: z.date(),10 updatedAt: z.date()11});12 13// Compose schemas14const userBaseSchema = z.object({15 name: z.string(),Dynamic Schema Generation
1function createFilterSchema<T extends z.ZodTypeAny>(itemSchema: T) {2 return z.object({3 page: z.number().int().positive().default(1),4 limit: z.number().int().positive().max(100).default(10),5 filters: z.record(z.unknown()).optional(),6 sort: z.object({7 field: z.string(),8 order: z.enum(['asc', 'desc'])9 }).optional()10 });11}Validation Middleware Pattern
1// lib/middleware/validation.ts2import { z } from 'zod';3import { NextRequest, NextResponse } from 'next/server';4 5export function withValidation<T>(6 schema: z.ZodSchema<T>,7 handler: (req: NextRequest, data: T) => Promise<NextResponse>8) {9 return async (req: NextRequest) => {10 try {11 const body = await req.json();12 const validatedData = schema.parse(body);13 return handler(req, validatedData);14 } catch (error) {15 if (error instanceof z.ZodError) {Conclusion
Zod validation in Next.js provides a robust foundation for building type-safe, production-ready applications. From API routes to Server Actions, middleware to forms, Zod ensures your data is validated at every layer.
Key takeaways:
Start with basic validation and gradually adopt more advanced patterns as your application grows in complexity.
Enjoyed this article?
Support our work and help us create more free content for developers.
Stay Updated
Get the latest articles and updates delivered to your inbox.