Server Side

Server Routes

Execute Convex operations from Nuxt server routes, middleware, and webhooks.

These utilities are auto-imported in your server/ directory.

Important naming distinction: This page covers Nitro server code (server/api/*, server/middleware/*, webhooks, jobs). It does not cover Nuxt app route middleware (defineNuxtRouteMiddleware).

If you need Convex data inside defineNuxtRouteMiddleware() (for auth/permissions during navigation), use one-shot APIs:

  • client navigation: useConvexCall
  • SSR navigation: serverConvexQuery

useConvexQuery and useConvexPaginatedQuery are setup-scope composables and should not be used inside route middleware/plugins.

Available Functions

FunctionPurpose
serverConvexQueryExecute a query
serverConvexMutationExecute a mutation
serverConvexActionExecute an action

serverConvexQuery

Execute a one-off query from server-side code.

server/api/posts.get.ts
import { api } from '~~/convex/_generated/api'

export default defineEventHandler(async (event) => {
  const posts = await serverConvexQuery(event, api.posts.list, { status: 'published' })

  return posts
})

API Reference

function serverConvexQuery<Query>(
  event: H3Event,
  query: Query,
  args?: FunctionArgs<Query>,
  options?: ServerConvexOptions,
): Promise<FunctionReturnType<Query>>
ParameterTypeDescription
eventH3EventCurrent Nitro event (used for config + auth)
queryFunctionReferenceQuery function reference
argsobjectQuery arguments
optionsServerConvexOptionsOptional auth policy/token override

serverConvexMutation

Execute a mutation from server-side code.

server/api/tasks/complete.post.ts
import { api } from '~~/convex/_generated/api'

export default defineEventHandler(async (event) => {
  const body = await readBody(event)

  await serverConvexMutation(event, api.tasks.complete, { taskId: body.taskId })

  return { success: true }
})

API Reference

function serverConvexMutation<Mutation>(
  event: H3Event,
  mutation: Mutation,
  args?: FunctionArgs<Mutation>,
  options?: ServerConvexOptions,
): Promise<FunctionReturnType<Mutation>>

serverConvexAction

Execute an action from server-side code.

server/api/reports/generate.post.ts
import { api } from '~~/convex/_generated/api'

export default defineEventHandler(async (event) => {
  const body = await readBody(event)

  const report = await serverConvexAction(event, api.reports.generate, {
    month: body.month,
    year: body.year,
  })

  return report
})

API Reference

function serverConvexAction<Action>(
  event: H3Event,
  action: Action,
  args?: FunctionArgs<Action>,
  options?: ServerConvexOptions,
): Promise<FunctionReturnType<Action>>

ServerConvexOptions

Options available for all fetch functions:

interface ServerConvexOptions {
  auth?: 'auto' | 'required' | 'none'
  authToken?: string
}
Enable module-level logging via the logging config option for debugging server-side operations.

Patterns

Webhook Handler

server/api/webhooks/stripe.post.ts
import { api } from '~~/convex/_generated/api'
import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export default defineEventHandler(async (event) => {
  const body = await readRawBody(event)
  const signature = getHeader(event, 'stripe-signature')!

  // Verify webhook
  const stripeEvent = stripe.webhooks.constructEvent(
    body!,
    signature,
    process.env.STRIPE_WEBHOOK_SECRET!,
  )

  // Handle the event
  switch (stripeEvent.type) {
    case 'checkout.session.completed': {
      const session = stripeEvent.data.object
      await serverConvexMutation(event, api.subscriptions.activate, {
        userId: session.metadata!.userId,
        stripeSubscriptionId: session.subscription as string,
      })
      break
    }

    case 'customer.subscription.deleted': {
      const subscription = stripeEvent.data.object
      await serverConvexMutation(event, api.subscriptions.cancel, {
        stripeSubscriptionId: subscription.id,
      })
      break
    }
  }

  return { received: true }
})

Authenticated API Route

server/api/user/profile.get.ts
import { api } from '~~/convex/_generated/api'

export default defineEventHandler(async (event) => {
  const config = useRuntimeConfig()

  // Get auth token from session cookie
  const sessionCookie = getCookie(event, 'better-auth.session_token')
  if (!sessionCookie) {
    throw createError({ statusCode: 401, message: 'Unauthorized' })
  }

  // Exchange session for JWT token
  const tokenResponse = await $fetch<{ token?: string }>(
    `${config.public.convex.siteUrl}/api/auth/convex/token`,
    { headers: { Cookie: `better-auth.session_token=${sessionCookie}` } },
  )

  if (!tokenResponse?.token) {
    throw createError({ statusCode: 401, message: 'Invalid session' })
  }

  // Fetch with auth
  const profile = await serverConvexQuery(
    event,
    api.users.getProfile,
    {},
    { authToken: tokenResponse.token },
  )

  return profile
})

Background Job Trigger

server/api/cron/daily-digest.post.ts
import { api } from '~~/convex/_generated/api'

export default defineEventHandler(async (event) => {
  // Verify cron secret
  const cronSecret = getHeader(event, 'x-cron-secret')
  if (cronSecret !== process.env.CRON_SECRET) {
    throw createError({ statusCode: 401 })
  }

  // Trigger digest generation
  const result = await serverConvexAction(event, api.notifications.sendDailyDigests, {})

  return { sent: result.count }
})

Server Middleware

server/middleware/analytics.ts
import { api } from '~~/convex/_generated/api'

export default defineEventHandler(async (event) => {
  // Only track page views
  if (!event.path.startsWith('/api/')) {
    // Fire and forget - don't await
    serverConvexMutation(event, api.analytics.trackPageView, {
      path: event.path,
      userAgent: getHeader(event, 'user-agent') ?? '',
      timestamp: Date.now(),
    }).catch(() => {
      // Ignore errors - analytics shouldn't break the request
    })
  }
})

External API Integration

server/api/import/github.post.ts
import { api } from '~~/convex/_generated/api'

export default defineEventHandler(async (event) => {
  const { repoUrl } = await readBody(event)

  // Fetch from external API
  const [owner, repo] = repoUrl.replace('https://github.com/', '').split('/')
  const issues = await $fetch(`https://api.github.com/repos/${owner}/${repo}/issues`, {
    headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` },
  })

  // Store in Convex
  const imported = await serverConvexMutation(event, api.issues.importFromGithub, {
    repoUrl,
    issues: issues.map((i: any) => ({
      title: i.title,
      body: i.body,
      githubId: i.id,
    })),
  })

  return { imported: imported.count }
})

Error Handling

export default defineEventHandler(async (event) => {
  try {
    const result = await serverConvexMutation(event, api.tasks.create, { text: 'New task' })
    return result
  } catch (error) {
    // Convex errors have a message property
    if (error instanceof Error) {
      throw createError({
        statusCode: 400,
        message: error.message,
      })
    }
    throw error
  }
})

Debugging

Enable logging in your nuxt.config.ts to debug server-side operations:

nuxt.config.ts
export default defineNuxtConfig({
  convex: {
    url: process.env.CONVEX_URL,
    logging: {
      enabled: true, // Enable canonical log events
    },
  },
})

Server-side operations emit operation:complete events:

[better-convex-nuxt] ▷ query ✓ api.posts.list 45ms
[better-convex-nuxt] ▷ mutation ✓ api.tasks.create 67ms

See Logging for full configuration options.


TypeScript

Full type inference:

// Return type is inferred from query
const posts = await serverConvexQuery(event, api.posts.list, { status: 'published' })
// posts: Post[]

// Args are typed
await serverConvexMutation(
  event,
  api.posts.create,
  { title: 123 }, // Type error: title should be string
)