Server Side

Server Routes

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

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

Available Functions

FunctionPurpose
fetchQueryExecute a query
fetchMutationExecute a mutation
fetchActionExecute an action

fetchQuery

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 config = useRuntimeConfig()

  const posts = await fetchQuery(
    config.public.convex.url,
    api.posts.list,
    { status: 'published' }
  )

  return posts
})

API Reference

function fetchQuery<Query>(
  convexUrl: string,
  query: Query,
  args?: FunctionArgs<Query>,
  options?: FetchOptions
): Promise<FunctionReturnType<Query>>
ParameterTypeDescription
convexUrlstringConvex deployment URL
queryFunctionReferenceQuery function reference
argsobjectQuery arguments
optionsFetchOptionsOptional: auth token

fetchMutation

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 config = useRuntimeConfig()
  const body = await readBody(event)

  await fetchMutation(
    config.public.convex.url,
    api.tasks.complete,
    { taskId: body.taskId }
  )

  return { success: true }
})

API Reference

function fetchMutation<Mutation>(
  convexUrl: string,
  mutation: Mutation,
  args?: FunctionArgs<Mutation>,
  options?: FetchOptions
): Promise<FunctionReturnType<Mutation>>

fetchAction

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 config = useRuntimeConfig()
  const body = await readBody(event)

  const report = await fetchAction(
    config.public.convex.url,
    api.reports.generate,
    { month: body.month, year: body.year }
  )

  return report
})

API Reference

function fetchAction<Action>(
  convexUrl: string,
  action: Action,
  args?: FunctionArgs<Action>,
  options?: FetchOptions
): Promise<FunctionReturnType<Action>>

FetchOptions

Options available for all fetch functions:

interface FetchOptions {
  /**
   * Auth token for authenticated operations.
   * If not provided, runs as unauthenticated.
   */
  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 config = useRuntimeConfig()
  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 fetchMutation(
        config.public.convex.url,
        api.subscriptions.activate,
        {
          userId: session.metadata!.userId,
          stripeSubscriptionId: session.subscription as string,
        }
      )
      break
    }

    case 'customer.subscription.deleted': {
      const subscription = stripeEvent.data.object
      await fetchMutation(
        config.public.convex.url,
        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 fetchQuery(
    config.public.convex.url,
    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) => {
  const config = useRuntimeConfig()

  // 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 fetchAction(
    config.public.convex.url,
    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/')) {
    const config = useRuntimeConfig()

    // Fire and forget - don't await
    fetchMutation(
      config.public.convex.url,
      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 config = useRuntimeConfig()
  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 fetchMutation(
    config.public.convex.url,
    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) => {
  const config = useRuntimeConfig()

  try {
    const result = await fetchMutation(
      config.public.convex.url,
      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 fetchQuery(
  config.public.convex.url,
  api.posts.list,
  { status: 'published' }
)
// posts: Post[]

// Args are typed
await fetchMutation(
  config.public.convex.url,
  api.posts.create,
  { title: 123 }  // Type error: title should be string
)