Handler Overrides

Handler Overrides

Note: defineAutoApiHandler is deprecated. Use createEndpoint() instead, which provides Zod body/query validation, typed context, response formatting, and full plugin middleware support.

Sometimes you need custom endpoint logic while preserving the auth/authz/validation pipeline. Use createEndpoint (recommended) or the legacy defineAutoApiHandler for this.

Basic Usage

Create a custom endpoint that reuses the auto-api pipeline:

// server/api/users/[id]/stats.get.ts
import { defineAutoApiHandler } from '@websideproject/nuxt-auto-api/utils'
import { eq, count } from 'drizzle-orm'
import { users, posts, comments } from '../../database/schema'

export default defineAutoApiHandler({
  async execute(context) {
    const userId = parseInt(context.params.id)

    // Custom query logic
    const [user, postStats, commentStats] = await Promise.all([
      context.db.query.users.findFirst({
        where: eq(users.id, userId),
      }),
      context.db
        .select({ count: count() })
        .from(posts)
        .where(eq(posts.userId, userId)),
      context.db
        .select({ count: count() })
        .from(comments)
        .where(eq(comments.userId, userId)),
    ])

    if (!user) {
      throw createError({ statusCode: 404, message: 'User not found' })
    }

    // Object-level auth check (reuses config from registry)
    if (context.objectLevelCheck) {
      const authorized = await context.objectLevelCheck(user, context)
      if (!authorized) {
        throw createError({ statusCode: 403, message: 'Forbidden' })
      }
    }

    return {
      data: {
        ...user,
        stats: {
          postCount: postStats[0].count,
          commentCount: commentStats[0].count,
        },
      },
    }
  },
})

Result:

GET /api/users/123/stats
  • ✅ Authentication runs (if configured)
  • ✅ Operation-level authorization runs
  • ✅ Validation runs (query params)
  • ✅ Custom logic executes
  • ✅ Object-level authorization (manual call)

Context Object

The context object provides access to:

interface HandlerContext {
  db: any                           // Database instance
  schema: any                       // Drizzle schema
  user: AuthUser | null            // Authenticated user
  permissions: string[]            // User permissions
  params: Record<string, string>   // Route params
  query: Record<string, any>       // Query params
  validated: {                     // Validated data
    body?: any
    query?: any
  }
  event: H3Event                   // H3 event
  resource: string                 // Resource name
  operation: string                // Operation type
  objectLevelCheck?: Function      // Object-level auth function
  listFilter?: Function            // SQL-level list filter (see better-auth docs)
  tenant?: {                       // Multi-tenancy info
    id: string | number
    field: string
    canAccessAllTenants: boolean
  }
}

Skip Authorization

For public endpoints, skip authorization:

export default defineAutoApiHandler({
  skipAuthorization: true,
  async execute(context) {
    // No auth required
    return { data: { message: 'Public data' } }
  },
})

Skip Validation

For custom validation logic:

export default defineAutoApiHandler({
  skipValidation: true,
  async execute(context) {
    const body = await readBody(context.event)

    // Custom validation
    if (!body.customField) {
      throw createError({ statusCode: 400, message: 'customField required' })
    }

    // ... custom logic
  },
})

Transform Response

Transform the result before returning:

export default defineAutoApiHandler({
  async execute(context) {
    const users = await context.db.query.users.findMany()
    return users
  },
  transform(result, context) {
    // Add metadata
    return {
      data: result,
      meta: {
        count: result.length,
        timestamp: new Date().toISOString(),
      },
    }
  },
})

Common Patterns

Bulk Operations

// server/api/users/bulk.post.ts
export default defineAutoApiHandler({
  async execute(context) {
    const users = context.validated.body.users // array of users

    // Batch insert
    const created = await context.db.insert(users).values(users).returning()

    return { data: created }
  },
})

Aggregations

// server/api/posts/stats.get.ts
export default defineAutoApiHandler({
  async execute(context) {
    const stats = await context.db
      .select({
        total: count(),
        published: count(posts.published),
      })
      .from(posts)

    return { data: stats[0] }
  },
})

Custom Queries

// server/api/users/search.get.ts
export default defineAutoApiHandler({
  async execute(context) {
    const { q } = context.validated.query

    const results = await context.db.query.users.findMany({
      where: or(
        like(users.name, `%${q}%`),
        like(users.email, `%${q}%`)
      ),
      limit: 20,
    })

    return { data: results }
  },
})

File Uploads

// server/api/users/[id]/avatar.post.ts
export default defineAutoApiHandler({
  skipValidation: true, // File uploads need custom handling
  async execute(context) {
    const userId = context.params.id
    const files = await readMultipartFormData(context.event)

    if (!files || files.length === 0) {
      throw createError({ statusCode: 400, message: 'No file uploaded' })
    }

    const file = files[0]
    const avatarUrl = await uploadToS3(file)

    // Update user
    await context.db
      .update(users)
      .set({ avatarUrl })
      .where(eq(users.id, userId))

    return { data: { avatarUrl } }
  },
})

Relations & Includes

// server/api/users/[id]/full.get.ts
export default defineAutoApiHandler({
  async execute(context) {
    const userId = context.params.id

    const user = await context.db.query.users.findFirst({
      where: eq(users.id, userId),
      with: {
        posts: {
          with: {
            comments: true,
          },
        },
        profile: true,
      },
    })

    return { data: user }
  },
})

When to Use Handler Overrides

Use handler overrides for:

  • Custom business logic
  • Complex queries or aggregations
  • Bulk operations
  • File uploads
  • Third-party API calls
  • Custom response formats

Don't use handler overrides for:

  • Simple CRUD - use auto-generated endpoints
  • Adding validation - use validation schemas
  • Authorization rules - use auth config
  • Field filtering - use query params (?fields=name,email)

Best Practices

  1. Preserve the pipeline - Keep auth/authz/validation unless absolutely necessary
  2. Reuse object-level checks - Call context.objectLevelCheck when needed
  3. Type safety - Use TypeScript for context and return types
  4. Error handling - Use createError for consistent error responses
  5. Document endpoints - Add JSDoc comments explaining custom behavior

Advanced: Middleware Composition

You can compose multiple handlers:

// Reusable middleware
async function requireOwnership(context: HandlerContext, resourceId: string) {
  const resource = await context.db.query.users.findFirst({
    where: eq(users.id, resourceId),
  })

  if (resource.userId !== context.user.id && !context.permissions.includes('admin')) {
    throw createError({ statusCode: 403, message: 'Not the owner' })
  }

  return resource
}

// Use in handler
export default defineAutoApiHandler({
  async execute(context) {
    const post = await requireOwnership(context, context.params.id)

    // ... custom logic with guaranteed ownership
  },
})

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.