Validation
Validation
@websideproject/nuxt-auto-api uses Zod for validation with automatic schema generation from Drizzle tables via drizzle-zod.
Automatic Validation
By default, validation schemas are auto-generated from your Drizzle tables:
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
email: text('email').notNull(),
name: text('name'),
})
This automatically validates:
- Required fields (
emailis required,nameis optional) - Field types (integer, text, boolean, etc.)
- Enum values (if defined)
Custom Validation
For custom rules, create validation schemas with drizzle-zod:
// server/validation/users.ts
import { createInsertSchema } from 'drizzle-zod'
import { z } from 'zod'
import { users } from '../database/schema'
const baseInsertSchema = createInsertSchema(users)
export const usersValidation = {
create: baseInsertSchema.extend({
email: z.string().email().toLowerCase().min(1),
name: z.string().min(2).max(100),
role: z.enum(['user', 'admin', 'editor']).default('user'),
}),
update: baseInsertSchema.partial().extend({
email: z.string().email().toLowerCase().optional(),
name: z.string().min(2).max(100).optional(),
}),
query: z.object({
filter: z.record(z.any()).optional(),
sort: z.union([z.string(), z.array(z.string())]).optional(),
fields: z.union([z.string(), z.array(z.string())]).optional(),
include: z.union([z.string(), z.array(z.string())]).optional(),
page: z.number().int().positive().optional(),
limit: z.number().int().positive().max(100).optional(),
}).optional(),
}
Register Validation
Register validation schemas in your module:
// modules/base/index.ts
nuxt.hook('autoApi:registerSchema', (registry) => {
registry.register('users', {
schema: createModuleImport(resolver.resolve('../../server/database/schema'), 'users'),
validation: createModuleImport(
resolver.resolve('../../server/validation/users'),
'usersValidation'
),
})
})
Validation Errors
Invalid requests return 400 with Zod error details:
Request:
POST /api/users
{
"email": "invalid-email",
"name": "A"
}
Response:
{
"statusCode": 400,
"message": "Validation error",
"data": {
"errors": [
{
"code": "invalid_string",
"path": ["email"],
"message": "Invalid email"
},
{
"code": "too_small",
"path": ["name"],
"message": "String must contain at least 2 character(s)",
"minimum": 2
}
]
}
}
Query Parameter Validation
Query parameters are validated by default:
// Invalid: limit too high
GET /api/users?limit=1000
// Response: 400 - limit must be <= 100
// Invalid: page must be positive
GET /api/users?page=-1
// Response: 400 - page must be positive
// Valid
GET /api/users?limit=50&page=2&sort=-createdAt
Custom Query Validation
Override query validation in your schema:
export const usersValidation = {
// ... create, update schemas
query: z.object({
filter: z.object({
role: z.enum(['user', 'admin', 'editor']).optional(),
verified: z.boolean().optional(),
}).optional(),
sort: z.enum(['name', '-name', 'createdAt', '-createdAt']).optional(),
limit: z.number().int().positive().max(50).optional(), // Lower limit
}),
}
Accessing Validated Data
In custom handlers, use context.validated:
import { defineAutoApiHandler } from '@websideproject/nuxt-auto-api/utils'
export default defineAutoApiHandler({
async execute(context) {
// Access validated body
const validatedBody = context.validated.body
console.log(validatedBody.email) // Guaranteed to be valid email
// Access validated query
const validatedQuery = context.validated.query
console.log(validatedQuery.limit) // Guaranteed to be valid number
// ... custom logic
},
})
Skip Validation
In rare cases, skip validation for custom handlers:
export default defineAutoApiHandler({
skipValidation: true,
async execute(context) {
// No validation performed
const body = await readBody(context.event)
// ... custom logic without validation
},
})
Best Practices
- Use drizzle-zod for base schemas - keeps validation in sync with database
- Extend, don't replace - build on auto-generated schemas
- Validate at boundaries - user input, not internal operations
- Use TypeScript - get type inference from Zod schemas
- Test validation - write tests for edge cases
Advanced Patterns
Conditional Validation
const createUserSchema = baseInsertSchema.extend({
email: z.string().email(),
role: z.enum(['user', 'admin']),
}).refine(
(data) => {
// Admin users must have @company.com email
if (data.role === 'admin') {
return data.email.endsWith('@company.com')
}
return true
},
{
message: 'Admin users must use company email',
path: ['email'],
}
)
Async Validation
const createUserSchema = baseInsertSchema.extend({
email: z.string().email(),
}).refine(
async (data) => {
// Check if email already exists
const existing = await db.query.users.findFirst({
where: eq(users.email, data.email),
})
return !existing
},
{
message: 'Email already exists',
path: ['email'],
}
)
Transform Data
const createUserSchema = baseInsertSchema.extend({
email: z.string().email().transform(val => val.toLowerCase()),
name: z.string().transform(val => val.trim()),
})
Multi-Tenancy
This guide covers building multi-tenant SaaS applications with organization-level permissions, where users can have different roles across organizations.
Rate Limiting
Rate limiting is provided as a built-in plugin to protect your API from abuse and ensure fair usage. It registers as pre-auth middleware that runs automatically on every API request.