These utilities are auto-imported in your server/ directory.
| Function | Purpose |
|---|---|
fetchQuery | Execute a query |
fetchMutation | Execute a mutation |
fetchAction | Execute an action |
Execute a one-off query from server-side code.
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
})
function fetchQuery<Query>(
convexUrl: string,
query: Query,
args?: FunctionArgs<Query>,
options?: FetchOptions
): Promise<FunctionReturnType<Query>>
| Parameter | Type | Description |
|---|---|---|
convexUrl | string | Convex deployment URL |
query | FunctionReference | Query function reference |
args | object | Query arguments |
options | FetchOptions | Optional: auth token |
Execute a mutation from server-side code.
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 }
})
function fetchMutation<Mutation>(
convexUrl: string,
mutation: Mutation,
args?: FunctionArgs<Mutation>,
options?: FetchOptions
): Promise<FunctionReturnType<Mutation>>
Execute an action from server-side code.
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
})
function fetchAction<Action>(
convexUrl: string,
action: Action,
args?: FunctionArgs<Action>,
options?: FetchOptions
): Promise<FunctionReturnType<Action>>
Options available for all fetch functions:
interface FetchOptions {
/**
* Auth token for authenticated operations.
* If not provided, runs as unauthenticated.
*/
authToken?: string
}
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 }
})
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
})
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 }
})
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
})
}
})
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 }
})
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
}
})
Enable logging in your nuxt.config.ts to debug server-side operations:
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.
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
)