For React apps built with Vite or Create React App, Courier provides createProxyQueryCaller for type-safe data fetching with React Query. Since these 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 React app Vite builds, it sets Courier up in direct mode — React Query hooks that call 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. For a backend proxy, as this page sets up, add one of the server guides. See Create Courier.
Installation
npm install @noverax/courier @noverax/courier-react @tanstack/react-queryDefine your API sources
Create a shared API definition. This file is used by both the React app and the backend proxy:
import { courier, defineApi, defineGroup } from "@noverax/courier/define"
const { endpoint } = courier()
type Product = { id: number; name: string; price: number }
type ProductList = { data: Product[]; total: number }
const products = defineGroup({
prefix: "products",
endpoints: {
getAll: endpoint.get("").variables<{ page?: number; category?: string }>().response<ProductList>(),
getById: endpoint.get(":id").variables<{ id: number }>().response<Product>(),
create: endpoint.post("").variables<{ name: string; price: number }>().response<Product>(),
},
})
export const api = defineApi({
baseURL: process.env.API_URL ?? "http://localhost:4000",
groups: { products },
})
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 the query caller
import { createProxyQueryCaller } from "@noverax/courier-react"
import type { api } from "../../shared/sources"
export const q = createProxyQueryCaller<typeof api>("myApi", {
proxyPath: { http: "http://localhost:3001/api/request" },
})Step 3: Set up QueryClientProvider
Wrap your app with the React Query provider:
import { StrictMode } from "react"
import { createRoot } from "react-dom/client"
import { QueryClient, QueryClientProvider } from "@noverax/courier-react"
import App from "./App"
const queryClient = new QueryClient()
createRoot(document.getElementById("root")!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</StrictMode>
)Step 4: Use in components
import { q } from "@/lib/query"
export function ProductList() {
const { data, isLoading, error } = q.products.useQuery("getAll", {
variables: { page: 1, category: "electronics" },
})
if (isLoading) return <p>Loading...</p>
if (error) return <p>Error: {error.message}</p>
return (
<div>
<h2>Products ({data?.total})</h2>
<ul>
{data?.data.map((product) => (
<li key={product.id}>
{product.name} — ${product.price}
</li>
))}
</ul>
</div>
)
}import { q } from "@/lib/query"
import { useState } from "react"
import { useQueryClient } from "@noverax/courier-react"
export function CreateProduct() {
const queryClient = useQueryClient()
const [name, setName] = useState("")
const [price, setPrice] = useState("")
const { mutate, isPending } = q.products.useMutation("create", {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["myApi", "products:getAll"] })
setName("")
setPrice("")
},
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
mutate({ name, price: Number(price) })
}}
>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Product name"
required
/>
<input
value={price}
onChange={(e) => setPrice(e.target.value)}
type="number"
placeholder="Price"
required
/>
<button type="submit" disabled={isPending}>
{isPending ? "Creating..." : "Add Product"}
</button>
</form>
)
}CORS and cookies
Make sure your proxy backend has CORS configured to allow requests from your React app's origin. If your backend uses cookies for auth, set credentials: true in the CORS config and credentials: "include" in the caller defaults, and name cookie and set-cookie on the source or the endpoints that carry the session — see Cookies.
Project structure
shared/
sources.ts # courier + defineGroup + defineApi (shared)
server/
index.ts # Express/Hono/Fastify with courierHandler
src/
lib/query.ts # createProxyQueryCaller<typeof api>("myApi")
components/
ProductList.tsx # q.products.useQuery usage
CreateProduct.tsx # q.products.useMutation usage
main.tsx # QueryClientProvider setupEnvironment variables
Use a .env file to configure the proxy URL for different environments:
VITE_PROXY_URL=http://localhost:3001/api/requestimport { createProxyQueryCaller } from "@noverax/courier-react"
import type { api } from "../../shared/sources"
export const q = createProxyQueryCaller<typeof api>("myApi", {
proxyPath: { http: import.meta.env.VITE_PROXY_URL },
})