Courier works with Nuxt through the @noverax/courier-nuxt adapter. Use courierHandler for the server proxy route, createProxyCaller for client-side requests, and createCaller for server-side data fetching.
In one command
npx @noverax/create-courierIn a Nuxt app it adds what this page sets up by hand — the server route, the Nitro plugin that registers your APIs, and Studio's module — with a typed example API, its mock, and a page at /courier. See Create Courier.
Installation
npm install @noverax/courier @noverax/courier-nuxtDefine your API sources
Create a shared API definition:
import { courier, defineApi, defineGroup } from "@noverax/courier/define"
const { endpoint } = courier()
type Post = { id: number; title: string; body: string }
type PostList = { data: Post[]; total: number }
const posts = defineGroup({
prefix: "posts",
endpoints: {
getAll: endpoint.get("").variables<{ page?: number }>().response<PostList>(),
getById: endpoint.get(":id").variables<{ id: number }>().response<Post>(),
create: endpoint.post("").variables<{ title: string; body: string }>().response<Post>(),
remove: endpoint.del(":id").variables<{ id: number }>().response<null>(),
},
})
export const api = defineApi({
baseURL: process.env.API_URL ?? "http://localhost:4000",
groups: { posts },
})
export const sources = { myApi: api }Step 1: Server route
Create a Nuxt server route that proxies client requests to your backend:
import { registerSources } from "@noverax/courier"
import { sources } from "~/lib/sources"
export default defineNitroPlugin(() => {
registerSources(sources)
})import { courierHandler } from "@noverax/courier-nuxt"
export default courierHandler()The server/plugins/courier.ts Nitro plugin runs once at server start, populating the in-process registry that courierHandler() reads at request time. The route answers every method at /api/request, the proxy target Courier clients use: a read arrives as GET, a write as POST, and an SSE stream as GET.
Step 2: Client-side composable
Create a composable using createProxyCaller for client-side requests:
import { createProxyCaller } from "@noverax/courier"
import type { api } from "~/lib/sources"
export function useCaller() {
return createProxyCaller<typeof api>("api", {
proxyPath: { http: "/api/request" },
})
}useAsyncData runs on the server first. There the call does not leave the server: the proxy caller reaches the proxy in the same process, where the source is registered, so the path needs no address to resolve against, and the call carries the session of the user the page is rendered for once each request is served with its session at hand — see the user's session. In the browser the same path goes to the route over HTTP.
Use it in your pages and components:
<script setup lang="ts">
const caller = useCaller()
const { data: posts, status } = await useAsyncData("posts", () =>
caller.posts.getAll({ page: 1 })
)
</script>
<template>
<div>
<p v-if="status === 'pending'">Loading...</p>
<template v-else-if="posts">
<h1>Posts ({{ posts.total }})</h1>
<ul>
<li v-for="post in posts.data" :key="post.id">
{{ post.title }}
</li>
</ul>
</template>
</div>
</template>Step 3: Server-side data fetching
For server-side data fetching in event handlers, use createCaller directly:
import { createCaller } from "@noverax/courier"
import { api } from "~/lib/sources"
export const caller = createCaller(api)Use it in Nuxt server routes or event handlers:
import { caller } from "~/lib/server-caller"
export default defineEventHandler(async () => {
const posts = await caller.posts.getAll({ page: 1 })
return {
totalPosts: posts.total,
latestPosts: posts.data.slice(0, 5),
}
})Using with useFetch
You can also combine the server caller with useFetch for SSR data that hydrates on the client:
<script setup lang="ts">
const route = useRoute()
const { data: post } = useFetch(`/api/posts/${route.params.id}`)
</script>
<template>
<article v-if="post">
<h1>{{ post.title }}</h1>
<p>{{ post.body }}</p>
</article>
</template>import { caller } from "~/lib/server-caller"
export default defineEventHandler(async (event) => {
const id = Number(getRouterParam(event, "id"))
return await caller.posts.getById({ id })
})Mutations
Handle mutations by calling the proxy caller from client-side event handlers:
<script setup lang="ts">
const caller = useCaller()
const title = ref("")
const body = ref("")
const isSubmitting = ref(false)
async function handleSubmit() {
isSubmitting.value = true
try {
const post = await caller.posts.create({
title: title.value,
body: body.value,
})
navigateTo(`/posts/${post.id}`)
} finally {
isSubmitting.value = false
}
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<input v-model="title" placeholder="Title" required />
<textarea v-model="body" placeholder="Body" required />
<button type="submit" :disabled="isSubmitting">
{{ isSubmitting ? "Creating..." : "Create Post" }}
</button>
</form>
</template>@tanstack/vue-query
For advanced caching and mutation management, consider using @tanstack/vue-query alongside createProxyCaller. Courier proxy callers return standard promises, so they work seamlessly with any data-fetching library.
The user's session
An API that keeps a session gets the user's session with every call a page's setup, a useAsyncData, a plugin or a server route makes, through every await, once Nitro serves each request with it at hand:
import { requestContextPlugin } from "@noverax/courier-nuxt"
export default defineNitroPlugin(requestContextPlugin)The plugin wraps the handler Nitro serves every request with, as Nitro does for its own asyncContext. The session cookies the API sets reach the browser on the response, during server rendering as from a server route. See User Sessions.
Realtime (SSE & WebSocket)
courierHandler() serves SSE streams as well as HTTP calls: a request that asks for text/event-stream gets a stream, and the stream ends when its client leaves. Point proxyPath.sse at the same route.
WebSocket channels are not served by the Nuxt adapter: Nitro's WebSocket layer (crossws) is still flagged experimental, and its behavior differs across deployment targets (node-server, bun, cloudflare, vercel-edge). Run a standalone Courier server alongside Nuxt for WebSocket endpoints, and point proxyPath.ws at it:
import { createProxyCaller } from "@noverax/courier"
import type { api } from "~/lib/sources"
export function useCaller() {
const { origin } = useRequestURL()
return createProxyCaller<typeof api>("api", {
proxyPath: {
http: "/api/request", // handled by Nuxt — in process while rendering
sse: `${origin}/api/request`, // handled by Nuxt — a stream always opens over HTTP
ws: "ws://localhost:4400/api/ws", // handled by standalone server
},
})
}Studio integration & appOrigin
Wire your Nuxt server into Studio with a single attachRuntime() call from a Nitro plugin. In development it loads courier.config.ts from disk, registers its sources, and opens the connection to Studio; in production the app's own registerSources() call registers them. The appOrigin field for Playground proxy SSE/WS resolution lives in courier.config.ts.runtime.appOrigin (or set the COURIER_APP_URL env var to override per-environment).
export default defineNitroPlugin(async () => {
if (import.meta.dev) {
const { attachRuntime } = await import("@noverax/courier-studio-core/runtime")
await attachRuntime()
}
})The runtime is imported inside the import.meta.dev check: a production build drops the whole branch, so .output carries no Studio code. Imported at the top of the file instead, the runtime would be copied into .output, with its Socket.IO client and its TypeScript loader, for a plugin that never runs there.
Alternatively, set COURIER_APP_URL instead of pinning appOrigin in the config:
COURIER_APP_URL=http://localhost:3000 nuxt devNo magic default
There is no localhost:3000 auto-fallback. If appOrigin is missing, clicking "Subscribe" on an SSE/WS endpoint in Studio Playground proxy mode throws ConfigurationError("APP_ORIGIN_NOT_CONFIGURED") with a pointer back to this section. The correct value depends on your runtime — custom port, Docker host, HTTPS, reverse proxy — and a wrong guess would silently route requests to nowhere.
Project structure
composables/
useCaller.ts # useCaller() → createProxyCaller<typeof api>("api")
lib/
sources.ts # defineApi + defineGroup + endpoint
server-caller.ts # createCaller(api)
server/
api/
request.ts # courierHandler()
dashboard.get.ts # server-side with createCaller
posts/[id].get.ts # dynamic server route
pages/
posts.vue # client-side with useAsyncData
posts/create.vue # mutations
posts/[id].vue # dynamic page with useFetch