Handler Overrides
Handler Overrides
Note:
defineAutoApiHandleris deprecated. UsecreateEndpoint()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
- Preserve the pipeline - Keep auth/authz/validation unless absolutely necessary
- Reuse object-level checks - Call
context.objectLevelCheckwhen needed - Type safety - Use TypeScript for context and return types
- Error handling - Use
createErrorfor consistent error responses - 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
},
})
Plugin Catalog
This document lists all shipped plugins for @websideproject/nuxt-auto-api.
Cloudflare D1
Nuxt Auto API fully supports Cloudflare D1, allowing you to deploy your API to the edge with a serverless SQLite database.