Custom Endpoints
Custom Endpoints
@websideproject/nuxt-auto-api provides two approaches for building custom server endpoints that integrate with the auto-api pipeline: createEndpoint() for full pipeline integration, and standalone helpers for lightweight use in regular Nitro handlers.
createEndpoint
createEndpoint() is the recommended way to build custom endpoints. It handles the full middleware pipeline, Zod validation, response formatting, and serialization.
Resource-Bound Endpoint
When you specify a resource, the endpoint uses the full auto-api pipeline (authorization, validation, middleware):
// server/api/users/[id]/stats.get.ts
import { createEndpoint } from '@websideproject/nuxt-auto-api/utils'
import { eq, count } from 'drizzle-orm'
import { users, posts } from '../../database/schema'
export default createEndpoint({
resource: 'users',
operation: 'get',
async handler(ctx) {
const userId = parseInt(ctx.params.id)
const [user, postCount] = await Promise.all([
ctx.db.query.users.findFirst({ where: eq(users.id, userId) }),
ctx.db.select({ count: count() }).from(posts).where(eq(posts.userId, userId)),
])
return { ...user, postCount: postCount[0].count }
},
})
Standalone Endpoint
Without a resource, the endpoint creates a lightweight context with database access and plugin middleware, but skips resource-specific authorization and validation:
// server/api/health.get.ts
import { createEndpoint } from '@websideproject/nuxt-auto-api/utils'
export default createEndpoint({
async handler(ctx) {
return { status: 'ok', timestamp: new Date().toISOString() }
},
responseFormat: 'raw', // Don't wrap in { data }
})
Body and Query Validation
Use Zod schemas for type-safe input validation:
// server/api/users/invite.post.ts
import { z } from 'zod'
import { createEndpoint } from '@websideproject/nuxt-auto-api/utils'
export default createEndpoint({
resource: 'users',
operation: 'create',
body: z.object({
email: z.string().email(),
role: z.enum(['user', 'editor', 'admin']).default('user'),
}),
query: z.object({
sendEmail: z.coerce.boolean().optional().default(true),
}),
async handler(ctx) {
// ctx.body and ctx.queryParams are typed
const { email, role } = ctx.body
const { sendEmail } = ctx.queryParams
const user = await ctx.db.insert(users).values({ email, role }).returning()
if (sendEmail) {
await sendInviteEmail(email)
}
return user[0]
},
})
Response Format
Control how the response is wrapped:
// 'auto' (default) - wraps in { data: result }
createEndpoint({
responseFormat: 'auto',
handler: async () => ({ name: 'Alice' }),
})
// Returns: { data: { name: 'Alice' } }
// 'raw' - passes through as-is
createEndpoint({
responseFormat: 'raw',
handler: async () => ({ name: 'Alice' }),
})
// Returns: { name: 'Alice' }
Transform
Transform the result before sending:
createEndpoint({
handler: async (ctx) => {
return await ctx.db.query.users.findMany()
},
transform: (users, ctx) => ({
users,
total: users.length,
requestedBy: ctx.user?.email,
}),
responseFormat: 'auto',
})
// Returns: { data: { users: [...], total: 5, requestedBy: 'admin@example.com' } }
Skip Authorization / Validation
createEndpoint({
resource: 'users',
skipAuthorization: true, // Public endpoint
skipValidation: true, // Custom validation in handler
handler: async (ctx) => { /* ... */ },
})
Standalone Helpers
For regular Nitro event handlers where you don't need the full pipeline, use the standalone helpers:
getAutoApiContext
Get a lightweight HandlerContext with database and plugin context extenders:
// server/api/dashboard.get.ts
export default defineEventHandler(async (event) => {
const ctx = await getAutoApiContext(event)
const userCount = await ctx.db.select({ count: count() }).from(users)
const postCount = await ctx.db.select({ count: count() }).from(posts)
return respondWith({
users: userCount[0].count,
posts: postCount[0].count,
})
})
validateBody / validateQuery
Validate request input with Zod and get typed results:
// server/api/contact.post.ts
import { z } from 'zod'
const ContactSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
message: z.string().min(10),
})
export default defineEventHandler(async (event) => {
const body = await validateBody(event, ContactSchema)
// body is typed as { name: string, email: string, message: string }
await sendContactEmail(body)
return respondWith({ sent: true })
})
respondWith / respondWithList / respondWithError
Standardized response formatting with serialization:
export default defineEventHandler(async (event) => {
const ctx = await getAutoApiContext(event)
const users = await ctx.db.select().from(usersTable)
// Single item
return respondWith(users[0])
// { data: { id: 1, name: 'Alice', createdAt: '2025-01-01T00:00:00.000Z' } }
// List with metadata
return respondWithList(users, { total: 100, page: 1, limit: 20 })
// { data: [...], meta: { total: 100, page: 1, limit: 20 } }
// Error
respondWithError(404, 'User not found')
respondWithError(422, 'Validation failed', { field: 'email', message: 'Already taken' })
})
getDb
Quick access to the database and adapter:
export default defineEventHandler(async (event) => {
const { db, adapter } = getDb()
// Use adapter for atomic operations
await adapter.atomic(async ({ tx }) => {
await tx.insert(orders).values(orderData)
await tx.insert(orderItems).values(itemsData)
})
return respondWith({ success: true })
})
All Available Helpers
| Helper | Description |
|---|---|
createEndpoint(options) | Full pipeline endpoint with Zod validation |
getAutoApiContext(event) | Lightweight HandlerContext for standalone handlers |
validateBody(event, zodSchema) | Validate request body, throw 400 on failure |
validateQuery(event, zodSchema) | Validate query params, throw 400 on failure |
respondWith(data) | Wrap in { data } with serialization |
respondWithList(data, meta?) | Wrap in { data, meta } with serialization |
respondWithError(status, message, details?) | Throw standardized error |
getDb() | Get { db, adapter } |
getResourceSchema(name) | Get table schema from registry |
getRegistry() | Get full resource registry |
serialize(data) | Serialize dates to ISO strings |
filterHidden(data, fields) | Remove hidden fields from response |
Migration from defineAutoApiHandler
defineAutoApiHandler is deprecated in favor of createEndpoint. Here's how to migrate:
// Before (deprecated)
export default defineAutoApiHandler({
async execute(context) {
return { data: { message: 'hello' } }
},
transform(result, context) {
return { ...result, extra: true }
},
skipAuthorization: true,
})
// After
export default createEndpoint({
skipAuthorization: true,
async handler(ctx) {
return { message: 'hello' }
},
transform(result, ctx) {
return { ...result, extra: true }
},
})
Key differences:
executeis nowhandlerhandlerreceivesEndpointContextwith typedbodyandqueryParams- Zod schemas for
bodyandqueryvalidation responseFormatoption ('auto' wraps in{ data }, 'raw' passes through)- Plugin middleware runs at all four stages
Database Adapters
@websideproject/nuxt-auto-api supports multiple database engines through a unified adapter layer. Each adapter normalizes engine-specific behavior like transactions, batch operations, and mutation count parsing.
Multi-Tenancy
This guide covers building multi-tenant SaaS applications with organization-level permissions, where users can have different roles across organizations.