# Nuxt

Nuxt integration with courierHandler for server routes, createProxyCaller for client-side, and createCaller for server-side data fetching.

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

```bash
npx @noverax/create-courier
```

In 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](/docs/create-courier).

## Installation

```bash
npm install @noverax/courier @noverax/courier-nuxt
```

## Define your API sources

Create the API definition the server route and Studio share. A page never imports it: it takes the routes Courier writes from it, so the definition stays out of the browser bundle.

```ts title="lib/sources.ts"
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",
  // A page may call these through the proxy; an endpoint no mark opens stays the server's
  expose: true,
  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:

```ts title="server/plugins/courier.ts"
import { registerSources } from "@noverax/courier"
import { sources } from "~/lib/sources"

export default defineNitroPlugin(() => {
  registerSources(sources)
})
```

```ts title="server/api/request.ts"
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 that may be cached arrives as `GET`, any other call as `POST`, and an SSE stream as `GET`.

## Step 2: Client-side composable

List your sources in `courier.config.ts`. From the endpoints they mark `.expose()`, Courier writes the routes a page's caller takes into `courier.routes.ts` beside it — Studio whenever it loads the config, or `npx @noverax/courier-cli routes` now; see [Exposing Endpoints](/docs/proxy-mode/expose):

```ts title="courier.config.ts"
import { defineConfig } from "@noverax/courier/config"
import { sources } from "./lib/sources"

export default defineConfig({ sources })
```

Create a composable using `createProxyCaller` for client-side requests, from the routes of `myApi`:

```ts title="composables/useCaller.ts"
import { createProxyCaller } from "@noverax/courier"
import { myApiRoutes } from "~/courier.routes"

export function useCaller() {
  return createProxyCaller(myApiRoutes, {
    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](#the-users-session). In the browser the same path goes to the route over HTTP.

Use it in your pages and components:

```vue title="pages/posts.vue"
<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:

```ts title="lib/server-caller.ts"
import { createCaller } from "@noverax/courier"
import { api } from "~/lib/sources"

export const caller = createCaller(api)
```

Use it in Nuxt server routes or event handlers:

```ts title="server/api/dashboard.get.ts"
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:

```vue title="pages/posts/[id].vue"
<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>
```

```ts title="server/api/posts/[id].get.ts"
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:

```vue title="pages/posts/create.vue"
<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:

```ts title="server/plugins/courier-request.ts"
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](/docs/server-side/session).

## 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](/docs/deployment/standalone-server) alongside Nuxt for WebSocket endpoints, and point `proxyPath.ws` at it:

```ts title="composables/useCaller.ts"
import { createProxyCaller } from "@noverax/courier"
import { myApiRoutes } from "~/courier.routes"

export function useCaller() {
  const { origin } = useRequestURL()
  return createProxyCaller(myApiRoutes, {
    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` <a id="studio-app-origin"></a>

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. Studio's Playground sends its proxy calls to the app's origin, which in development [Studio's Nuxt module](/docs/studio/overview#nuxt) hands Nitro as `nuxt dev` starts listening. Where Studio cannot see the server, set `courier.config.ts.runtime.appOrigin` or the `COURIER_APP_URL` env var — see [How the app learns its origin](/docs/api/config#app-origin).

```ts title="server/plugins/courier-studio.ts"
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:

```bash
COURIER_APP_URL=http://localhost:3000 nuxt dev
```

> **No magic default**
>
> There is no `localhost:3000` auto-fallback. Until the config, the environment or the server itself names the origin, a Playground call in proxy mode throws `ConfigurationError("APP_ORIGIN_NOT_CONFIGURED")` with a pointer to [how the app learns its origin](/docs/api/config#app-origin). 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

```
courier.config.ts           # defineConfig({ sources })
courier.routes.ts           # written by Studio from the .expose() marks
composables/
  useCaller.ts              # useCaller() → createProxyCaller(myApiRoutes)
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
```
