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
npx @noverax/create-courierIn 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
npm install @noverax/courierDefine your API sources
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:
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")
})Step 2: Create a composable
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
<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
<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:
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:
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
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