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.

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

HelperDescription
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:

  • execute is now handler
  • handler receives EndpointContext with typed body and queryParams
  • Zod schemas for body and query validation
  • responseFormat option ('auto' wraps in { data }, 'raw' passes through)
  • Plugin middleware runs at all four stages

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.