Validation

@websideproject/nuxt-auto-api uses Zod for validation with automatic schema generation from Drizzle tables via drizzle-zod.

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 (email is required, name is 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

  1. Use drizzle-zod for base schemas - keeps validation in sync with database
  2. Extend, don't replace - build on auto-generated schemas
  3. Validate at boundaries - user input, not internal operations
  4. Use TypeScript - get type inference from Zod schemas
  5. 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()),
})

Need a Landing Page?

Modern landing pages with optional modules (blog, docs, forms, i18n). Let's discuss your project.

Build Your MVP

Full-stack SaaS development. Expert in database design, multi-tenancy, and scalable architecture.

Deployment Help

Dockerize your backend, set up CI/CD pipelines, deploy to Cloudflare or Hetzner. Early-stage setup.

Suggest a SaaS Tool

Missing a calculator or tool? Suggest what you'd like to see on our site.