Skip to content

IntroductionWhy Courier?

Why Courier?

The problems Courier solves: boilerplate, type safety, proxy complexity, and scattered API logic — across any framework.

3 min read

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:

  1. Write fetch calls with manual URL construction, headers, and error handling
  2. Cast response types manually with no runtime guarantee
  3. Create separate API routes for every endpoint you want to proxy
  4. Duplicate type annotations between your fetch logic and your components
  5. Wire up React Query (or Vue composables, Svelte stores) with custom keys and fetch functions per endpoint

This leads to fragile, repetitive code:

Without Courier
// 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:

With Courier
// 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:

One definition, multiple modes
// 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

React Query comparison
// 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 } })
//            ^? User

Key Benefits

ProblemCourier Solution
Manual type castingEnd-to-end inference from the builder chain
Route per endpointSingle handler for all endpoints
Scattered fetch logicCentralized in defineApi/defineGroup
Framework lock-inCore works everywhere, adapters for 6+ frameworks
No runtime validationStandard Schema (Zod, Valibot, ArkType)
React Query boilerplateuseQuery / useMutation with auto inference
Upload progressBuilt-in XHR tracking with progress callback
Middleware per-endpointComposable 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