Validation
Custom validation schemas and adapters in Drizzle CRUD
Validation
Drizzle CRUD supports powerful validation through Standard Schema compatible libraries. By default, it integrates with Zod, but you can use any validation library that implements the Standard Schema interface.
Zod Integration
The easiest way to add validation is using the built-in Zod adapter:
import { drizzleCrud } from 'drizzle-crud'
import { zod } from 'drizzle-crud/zod'
const createCrud = drizzleCrud(db, {
validation: zod(),
})
Default Schema Generation
When using the Zod adapter, schemas are automatically generated from your Drizzle table definitions:
const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull(),
age: integer('age'),
isActive: boolean('is_active').default(true),
})
const userCrud = createCrud(users, {
// Schemas automatically generated:
// - insert: name, email, age?, isActive?
// - update: name?, email?, age?, isActive?
})
Custom Schemas
Override the default schemas with custom validation:
import { z } from 'zod'
const userCrud = createCrud(users, {
validation: zod({
insert: () =>
z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
age: z.number().min(13).max(120).optional(),
isActive: z.boolean().default(true),
}),
update: () =>
z.object({
name: z.string().min(2).max(50).optional(),
email: z.string().email().optional(),
age: z.number().min(13).max(120).optional(),
isActive: z.boolean().optional(),
}),
}),
})
Schema Types
Insert Schema
Validates data for create
and bulkCreate
operations:
const validation = zod({
insert: () =>
z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email format'),
password: z.string().min(8, 'Password must be at least 8 characters'),
}),
})
Update Schema
Validates data for update
operations:
const validation = zod({
update: () =>
z.object({
name: z.string().min(1).optional(),
email: z.string().email().optional(),
// Password not required for updates
}),
})
List Schema
Validates query parameters for list
operations:
const validation = zod({
list: () =>
z.object({
search: z.string().optional(),
page: z.number().min(1).optional(),
limit: z.number().min(1).max(100).optional(),
includeDeleted: z.boolean().optional(),
}),
})
Global vs Table-Level Validation
Global Validation
Set default validation for all tables:
const createCrud = drizzleCrud(db, {
validation: zod(), // Default for all tables
})
Table-Level Validation
Override validation for specific tables:
const userCrud = createCrud(users, {
validation: zod({
insert: () =>
z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
}),
}),
})
const postCrud = createCrud(posts, {
validation: zod({
insert: () =>
z.object({
title: z.string().min(5).max(200),
content: z.string().min(10),
}),
}),
})
Advanced Validation
Conditional Validation
Validate based on other field values:
const validation = zod({
insert: () =>
z.object({
type: z.enum(['individual', 'business']),
name: z.string().min(1),
companyName: z.string().optional(),
taxId: z.string().optional(),
}).refine(
(data) => {
// Require companyName and taxId for business accounts
if (data.type === 'business') {
return data.companyName && data.taxId
}
return true
},
{
message: 'Business accounts require company name and tax ID',
path: ['companyName'],
}
),
})
Cross-Field Validation
Validate relationships between fields:
const validation = zod({
insert: () =>
z.object({
startDate: z.date(),
endDate: z.date(),
title: z.string().min(1),
}).refine(
(data) => data.endDate > data.startDate,
{
message: 'End date must be after start date',
path: ['endDate'],
}
),
})
Async Validation
Validate against external services or databases:
const validation = zod({
insert: () =>
z.object({
email: z.string().email(),
username: z.string().min(3),
}).refine(
async (data) => {
// Check if email already exists
const existingUser = await db.query.users.findFirst({
where: eq(users.email, data.email),
})
return !existingUser
},
{
message: 'Email already in use',
path: ['email'],
}
),
})
Custom Validation Adapters
Create adapters for other validation libraries:
import type { ValidationAdapter } from 'drizzle-crud'
// Example adapter for Joi
function joi(): ValidationAdapter {
return {
validate: (schema, data) => {
const { error, value } = schema.validate(data)
if (error) {
throw new Error(error.message)
}
return value
},
generateInsertSchema: (table) => {
// Generate Joi schema from Drizzle table
return Joi.object({
// ... generate schema
})
},
generateUpdateSchema: (table) => {
// Generate Joi schema for updates
return Joi.object({
// ... generate schema
})
},
}
}
// Usage
const createCrud = drizzleCrud(db, {
validation: joi(),
})
Arktype Example
import { type } from 'arktype'
function arktype(): ValidationAdapter {
return {
validate: (schema, data) => {
const result = schema(data)
if (result.problems) {
throw new Error(result.problems[0].message)
}
return result.data
},
generateInsertSchema: (table) => {
return type({
name: 'string',
email: 'string',
age: 'number?',
})
},
generateUpdateSchema: (table) => {
return type({
name: 'string?',
email: 'string?',
age: 'number?',
})
},
}
}
Skipping Validation
Skip validation for trusted operations:
const user = await userCrud.create(
{
name: 'John Doe',
email: 'john@example.com',
},
{
skipValidation: true, // Skip schema validation
}
)
This is useful when:
- Data is already validated (e.g., in tRPC procedures)
- Importing data from trusted sources
- Internal system operations
Error Handling
Handle validation errors gracefully:
try {
await userCrud.create({
name: 'J', // Too short
email: 'invalid-email',
})
} catch (error) {
if (error instanceof z.ZodError) {
// Handle Zod validation errors
console.log(error.issues)
} else {
// Handle other errors
console.log(error.message)
}
}
Validation in Practice
User Registration
const userCrud = createCrud(users, {
validation: zod({
insert: () =>
z.object({
email: z.string().email(),
password: z.string()
.min(8, 'Password must be at least 8 characters')
.regex(/^(?=.*[A-Za-z])(?=.*\d)/, 'Password must contain letters and numbers'),
firstName: z.string().min(1, 'First name is required'),
lastName: z.string().min(1, 'Last name is required'),
age: z.number().min(13, 'Must be at least 13 years old'),
}),
}),
})
Product Catalog
const productCrud = createCrud(products, {
validation: zod({
insert: () =>
z.object({
name: z.string().min(1).max(100),
price: z.number().positive(),
category: z.enum(['electronics', 'clothing', 'books']),
sku: z.string().regex(/^[A-Z0-9-]+$/),
inStock: z.boolean().default(true),
}),
update: () =>
z.object({
name: z.string().min(1).max(100).optional(),
price: z.number().positive().optional(),
category: z.enum(['electronics', 'clothing', 'books']).optional(),
inStock: z.boolean().optional(),
}),
}),
})
Blog Posts
const postCrud = createCrud(posts, {
validation: zod({
insert: () =>
z.object({
title: z.string().min(5).max(200),
content: z.string().min(100),
tags: z.array(z.string()).max(5),
isPublished: z.boolean().default(false),
publishedAt: z.date().optional(),
}).refine(
(data) => {
// If published, must have publishedAt
if (data.isPublished) {
return data.publishedAt !== undefined
}
return true
},
{
message: 'Published posts must have a publish date',
path: ['publishedAt'],
}
),
}),
})
Best Practices
1. Use Descriptive Error Messages
const validation = zod({
insert: () =>
z.object({
email: z.string().email('Please enter a valid email address'),
name: z.string().min(2, 'Name must be at least 2 characters long'),
}),
})
2. Validate at the Right Level
// Good: Business logic validation in schema
const validation = zod({
insert: () =>
z.object({
age: z.number().min(13, 'Must be at least 13 years old'),
}),
})
// Avoid: Database constraint validation in schema
const validation = zod({
insert: () =>
z.object({
id: z.number().int().positive(), // Let database handle this
}),
})
3. Keep Schemas DRY
// Shared validation rules
const emailSchema = z.string().email()
const nameSchema = z.string().min(2).max(50)
const userValidation = zod({
insert: () =>
z.object({
email: emailSchema,
firstName: nameSchema,
lastName: nameSchema,
}),
})
Next Steps
- Transactions - Database transactions
- Soft Deletes - Soft delete configuration
- Bulk Operations - Efficient bulk operations