zod-validation
Zod Validation Patterns
Schema Location
features/<feature>/model/
├── <feature>-schemas.ts # Zod schemas + inferred types
└── <feature>-constants.ts # Constants used in schemas
Basic Schema
// account-schemas.ts
import { z } from 'zod'
export const AccountSchema = z.object({
id: z.string().ulid(),
userId: z.string().ulid(),
name: z.string().min(1).max(100),
balance: z.number().default(0),
deleted: z.boolean().default(false),
createdAt: z.date(),
updatedAt: z.date(),
})
export type Account = z.infer<typeof AccountSchema>
Input Schemas (Pick/Omit)
// Create - only user-provided fields
export const CreateAccountSchema = AccountSchema.pick({
name: true,
})
export type CreateAccountInput = z.infer<typeof CreateAccountSchema>
// Update - partial user fields
export const UpdateAccountSchema = AccountSchema.pick({
name: true,
balance: true,
}).partial()
export type UpdateAccountInput = z.infer<typeof UpdateAccountSchema>
Common Patterns
// Optional with default
z.string().default('')
z.number().default(0)
z.boolean().default(false)
// Nullable vs Optional
z.string().nullable() // string | null
z.string().optional() // string | undefined
z.string().nullish() // string | null | undefined
// Enums
export const StatusSchema = z.enum(['active', 'suspended', 'deleted'])
export type Status = z.infer<typeof StatusSchema>
// Literals
z.literal('draft')
// Union
z.union([z.string(), z.number()])
// Arrays
z.array(z.string())
z.string().array() // Same as above
// Records/Maps
z.record(z.string()) // { [key: string]: string }
Shared Schemas (@saas4dev/core)
import {
UlidSchema, // z.string().ulid()
EmailSchema, // z.string().email()
RequiredStringSchema, // z.string().min(1)
UrlSchema, // z.string().url()
PhoneSchema, // Phone validation
} from '@saas4dev/core'
Refinements
// Custom validation
const PasswordSchema = z.string()
.min(8)
.refine(
(val) => /[A-Z]/.test(val),
{ message: 'Must contain uppercase' }
)
.refine(
(val) => /[0-9]/.test(val),
{ message: 'Must contain number' }
)
// Cross-field validation
const DateRangeSchema = z.object({
startDate: z.date(),
endDate: z.date(),
}).refine(
(data) => data.endDate > data.startDate,
{ message: 'End date must be after start', path: ['endDate'] }
)
Transform
// Transform input
const TrimmedString = z.string().trim()
const LowerEmail = z.string().email().toLowerCase()
// Coerce types
z.coerce.number() // "123" -> 123
z.coerce.date() // "2024-01-01" -> Date
z.coerce.boolean() // "true" -> true
Error Handling
import { convertZodErrorsToKeyValue } from '@saas4dev/core'
try {
const data = Schema.parse(input)
} catch (error) {
if (error instanceof z.ZodError) {
const fieldErrors = convertZodErrorsToKeyValue(error)
// { name: 'Required', email: 'Invalid email' }
return { success: false, errors: fieldErrors }
}
}
// Safe parse (no throw)
const result = Schema.safeParse(input)
if (!result.success) {
const errors = convertZodErrorsToKeyValue(result.error)
}
Form Integration
// React Hook Form
import { zodResolver } from '@hookform/resolvers/zod'
const form = useForm<CreateAccountInput>({
resolver: zodResolver(CreateAccountSchema),
defaultValues: { name: '' },
})
Server Action Input
// ZSA validates automatically
export const createAction = authedProcedure
.createServerAction()
.input(CreateAccountSchema, { type: 'formData' })
.handler(async ({ input }) => {
// input is typed and validated
})
Constants Pattern
// account-constants.ts
export const ACCOUNT_MAX_NAME_LENGTH = 100
export const ACCOUNT_TYPES = ['checking', 'savings', 'credit'] as const
// Use in schema
const AccountSchema = z.object({
name: z.string().max(ACCOUNT_MAX_NAME_LENGTH),
type: z.enum(ACCOUNT_TYPES),
})
More from gilbertopsantosjr/fullstacknextjs
gs-tanstack-react-query
TanStack React Query for data fetching with Clean Architecture. Queries return DTOs, mutations call server actions. Use when working with useQuery, useMutation, cache invalidation, or integrating ZSA server actions.
9tanstack-react-query
TanStack React Query expert for data fetching and mutations in React applications. Use when working with useQuery, useMutation, cache invalidation, optimistic updates, query keys, or integrating server actions with React Query via @saas4dev/core hooks (useServerActionQuery, useServerActionMutation, useServerActionInfiniteQuery). Triggers on requests involving API data fetching, server state management, cache strategies, or converting fetch/useEffect patterns to React Query.
4gs-feature-architecture
Guide for implementing features in Clean Architecture OOP with Next.js. Use when planning new features, understanding the 4-layer structure (Domain, Application, Infrastructure, Presentation), or deciding where code should live.
3sst-infra
Guide for AWS serverless infrastructure using SST v3 (Serverless Stack). Use when configuring deployment, creating stacks, managing secrets, setting up CI/CD, or deploying Next.js applications to AWS Lambda with DynamoDB.
2gs-sst-infra
Guide for AWS serverless infrastructure using SST v3. Covers DynamoDB, Next.js deployment, Lambda handlers with Clean Architecture adapter pattern, and CI/CD configuration.
2feature-architecture
Guide for implementing features in a layered Next.js full-stack architecture. Use when planning new features, understanding the layer structure (Model, DAL, Service, Actions, Components, Pages), or deciding where code should live.
2