Building a web app that talks to a REST API sounds simple — until you need type safety, a proxy layer, cookie forwarding, error handling, upload progress, middleware, validation, and framework integration. Each of these adds boilerplate, and without a unified approach, your API logic scatters across dozens of files.
Courier solves this by giving you a single, typed API definition that works across any framework — with zero code generation.
The Problem
A typical frontend + REST API setup requires you to:
- Write
fetchcalls with manual URL construction, headers, and error handling - Cast response types manually with no runtime guarantee
- Create separate API routes for every endpoint you want to proxy
- Duplicate type annotations between your fetch logic and your components
- Wire up React Query (or Vue composables, Svelte stores) with custom keys and fetch functions per endpoint
This leads to fragile, repetitive code:
// Manual fetch — no type safety, lots of boilerplate
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/users/${id}`, {
headers: { Authorization: `Bearer ${token}` },
})
if (!res.ok) throw new Error("Failed to fetch user")
const user = (await res.json()) as User // manual cast, no guarantee
// Need a proxy? Write a route handler for every endpoint...With Courier, the same functionality is type-safe and zero-boilerplate:
// Type-safe, fully inferred
const user = await caller.users.getById({ id: "5" })
// ^? User — inferred from .response<User>()
// Proxy? One line in your server:
export const { GET, POST } = apiRoute() // or courierHandler()
// Done. Every endpoint works through this single route.Define Once, Use Everywhere
You define your API structure once using courier, defineGroup, and defineApi. Each endpoint declares its response and variables types. From that single definition, Courier gives you:
- Direct mode — server calls via
createCaller(api) - Proxy mode — browser calls via
createProxyCaller("api") - React Query — hooks via
createProxyQueryCaller("api") - Server Actions — server-action calls via
createAction(sources, "api")
All share the same types and autocomplete:
// Define once
const users = defineGroup({
prefix: "users",
endpoints: {
getAll: endpoint.get("").response<User[]>(),
getById: endpoint.get(":id").variables<{ id: string }>().response<User>(),
create: endpoint.post("").variables<CreateUserVars>().response<User>(),
},
})
// Direct — server-side
const onServer = await caller.users.getById({ id: "5" })
// Proxy — client-side
const inBrowser = await proxyCaller.users.getById({ id: "5" })
// React Query — same types again
const { data } = q.users.useQuery("getById", { variables: { id: "5" } })No code generation
Unlike tRPC or GraphQL codegen, Courier uses plain TypeScript functions as the source of truth. No generated files, no build steps. Rename a method and TypeScript catches every broken call site instantly.
One Proxy Route for Everything
Without Courier, proxying requests means creating a route for every backend endpoint. With Courier, a single handler covers all endpoints. It automatically:
- Validates the request origin (CSRF protection)
- Forwards the headers and the cookies you name, and nothing else
- Routes to the correct source, group, and method
- Handles errors with proper HTTP status codes
- Supports file uploads and multipart encoding
React Query Without the Boilerplate
// Traditional — manual setup per endpoint
const { data } = useQuery({
queryKey: ["users", id],
queryFn: async () => {
const res = await fetch(`/api/users/${id}`)
if (!res.ok) throw new Error("fetch failed")
return res.json() as Promise<User>
},
})
// Courier — one line, fully typed
const { data: user } = q.users.useQuery("getById", { variables: { id } })
// ^? UserKey Benefits
| Problem | Courier Solution |
|---|---|
| Manual type casting | End-to-end inference from the builder chain |
| Route per endpoint | Single handler for all endpoints |
| Scattered fetch logic | Centralized in defineApi/defineGroup |
| Framework lock-in | Core works everywhere, adapters for 6+ frameworks |
| No runtime validation | Standard Schema (Zod, Valibot, ArkType) |
| React Query boilerplate | useQuery / useMutation with auto inference |
| Upload progress | Built-in XHR tracking with progress callback |
| Middleware per-endpoint | Composable chain: retry, auth, logging, cache |
Who is it for?
Courier is designed for teams and projects that:
- Use any modern framework (Next.js, Nuxt, React, Vue, Hono, Express, etc.)
- Have a separate REST backend (Express, FastAPI, Rails, Go, Laravel — anything)
- Want type safety without adopting tRPC or GraphQL
- Need both client-side and server-side data fetching
- Value DX — autocomplete, compile-time errors, minimal boilerplate
Works with any REST backend
Courier does not require a specific backend framework. It works with any HTTP API that returns JSON. Your backend needs zero Courier-specific setup.
Next Steps
- Architecture — how Courier works under the hood
- Installation — add Courier to your project
- Quick Start — your first type-safe API call