Skip to content

Framework GuidesVue

Vue

Use Courier with Vue via createProxyCaller in composables with ref/reactive patterns.

3 min readproxy mode

For Vue apps (with Vite), Courier provides createProxyCaller for type-safe API calls. Since Vue apps don't have a built-in server, you need a separate backend (Express, Hono, or Fastify) to host the proxy route.

In one command

bash
npx @noverax/create-courier

In a Vue app Vite builds, it sets Courier up in direct mode — a client that calls your API straight from the browser, and Studio's plugin — with a typed example API, its mock, and a page of its own at /courier.html. In Nuxt it sets up the proxy route instead: see Nuxt. See Create Courier.

Installation

bash
npm install @noverax/courier

Define your API sources

shared/sources.ts
import { courier, defineApi, defineGroup } from "@noverax/courier/define"

const { endpoint } = courier()

type Task = { id: number; title: string; completed: boolean }

const tasks = defineGroup({
  prefix: "tasks",
  endpoints: {
    getAll: endpoint.get("").variables<{ status?: "all" | "active" | "completed" }>().response<Task[]>(),
    getById: endpoint.get(":id").variables<{ id: number }>().response<Task>(),
    create: endpoint.post("").variables<{ title: string }>().response<Task>(),
    toggle: endpoint.patch(":id").variables<{ id: number; completed: boolean }>().response<Task>(),
    remove: endpoint.del(":id").variables<{ id: number }>().response<null>(),
  },
})

export const api = defineApi({
  baseURL: process.env.API_URL ?? "http://localhost:4000",
  groups: { tasks },
})

export const sources = { myApi: api }

Step 1: Set up the proxy backend

You need a backend server to host the Courier proxy route. Here is an example using Express:

server/index.ts
import express from "express"
import cors from "cors"
import { registerSources } from "@noverax/courier"
import { courierHandler } from "@noverax/courier-express"
import { sources } from "../shared/sources"

registerSources(sources)

const app = express()

app.use(cors({ origin: "http://localhost:5173", credentials: true }))

// No body parser before this line: Courier reads the raw body itself, and a
// global `express.json()` would consume it first.

app.all("/api/request", courierHandler())

app.listen(3001, () => {
  console.log("Proxy server running on http://localhost:3001")
})

Choose your backend

You can use any supported adapter: Express, Hono, Fastify, or NestJS. The proxy route is framework-agnostic — only the adapter differs.

Step 2: Create a composable

src/composables/useCaller.ts
import { createProxyCaller } from "@noverax/courier"
import type { api } from "../../shared/sources"

export const caller = createProxyCaller<typeof api>("api", {
  proxyPath: { http: import.meta.env.VITE_PROXY_URL ?? "http://localhost:3001/api/request" },
})

Step 3: Use in components

Fetching data with ref

src/components/TaskList.vue
<script setup lang="ts">
import { ref, onMounted } from "vue"
import { caller } from "@/composables/useCaller"

type Task = { id: number; title: string; completed: boolean }

const tasks = ref<Task[]>([])
const isLoading = ref(true)
const error = ref<string | null>(null)

async function fetchTasks(status?: "all" | "active" | "completed") {
  isLoading.value = true
  error.value = null
  try {
    tasks.value = await caller.tasks.getAll({ status })
  } catch (e) {
    error.value = e instanceof Error ? e.message : "Failed to load tasks"
  } finally {
    isLoading.value = false
  }
}

onMounted(() => fetchTasks())
</script>

<template>
  <div>
    <div>
      <button @click="fetchTasks('all')">All</button>
      <button @click="fetchTasks('active')">Active</button>
      <button @click="fetchTasks('completed')">Completed</button>
    </div>

    <p v-if="isLoading">Loading...</p>
    <p v-else-if="error">{{ error }}</p>
    <ul v-else>
      <li v-for="task in tasks" :key="task.id">
        <span :class="{ completed: task.completed }">{{ task.title }}</span>
      </li>
    </ul>
  </div>
</template>

Mutations with reactive state

src/components/CreateTask.vue
<script setup lang="ts">
import { reactive } from "vue"
import { caller } from "@/composables/useCaller"

const emit = defineEmits<{ created: [] }>()

const form = reactive({
  title: "",
  isSubmitting: false,
  error: null as string | null,
})

async function handleSubmit() {
  form.isSubmitting = true
  form.error = null
  try {
    await caller.tasks.create({ title: form.title })
    form.title = ""
    emit("created")
  } catch (e) {
    form.error = e instanceof Error ? e.message : "Failed to create task"
  } finally {
    form.isSubmitting = false
  }
}
</script>

<template>
  <form @submit.prevent="handleSubmit">
    <input v-model="form.title" placeholder="New task" required />
    <button type="submit" :disabled="form.isSubmitting">
      {{ form.isSubmitting ? "Creating..." : "Add Task" }}
    </button>
    <p v-if="form.error" class="error">{{ form.error }}</p>
  </form>
</template>

Composable with auto-fetch

For reusable data fetching, create a generic composable:

src/composables/useCourierQuery.ts
import { ref, onMounted, type Ref } from "vue"
import { caller } from "./useCaller"

type Caller = typeof caller

export function useCourierQuery<T>(
  fn: (c: Caller) => Promise<T>
) {
  const data = ref<T | null>(null) as Ref<T | null>
  const isLoading = ref(true)
  const error = ref<string | null>(null)

  async function refetch() {
    isLoading.value = true
    error.value = null
    try {
      data.value = await fn(caller)
    } catch (e) {
      error.value = e instanceof Error ? e.message : "Request failed"
    } finally {
      isLoading.value = false
    }
  }

  onMounted(refetch)

  return { data, isLoading, error, refetch }
}

@tanstack/vue-query

For production apps with advanced caching, background refetching, and optimistic updates, use @tanstack/vue-query. Courier callers return standard promises, so they integrate directly:

ts
import { useQuery } from "@tanstack/vue-query"
import { caller } from "@/composables/useCaller"

const { data, isLoading } = useQuery({
  queryKey: ["tasks"],
  queryFn: () => caller.tasks.getAll({ status: "active" }),
})

Project structure

Project structure
shared/
  sources.ts                  # defineApi + defineGroup + endpoint
server/
  index.ts                    # Express/Hono/Fastify proxy backend
src/
  composables/
    useCaller.ts              # createProxyCaller<typeof api>("api")
    useCourierQuery.ts        # optional generic fetch composable
  components/
    TaskList.vue              # data fetching with ref
    CreateTask.vue            # mutations with reactive