@noverax/courier is a type-safe HTTP client that works with any JavaScript framework and any REST backend. Define your API once as plain TypeScript functions — get autocomplete, type inference, runtime validation, and middleware out of the box.
No codegen. No schema files. No changes to your backend.
How It Works
Define your API using courier, defineGroup, and defineApi. Each endpoint declares its response and variables types:
import { courier, defineApi, defineGroup } from "@noverax/courier/define"
const { endpoint } = courier()
type User = { id: string; name: string; email: string }
const users = defineGroup({
prefix: "users",
endpoints: {
getAll: endpoint.get("").response<User[]>(),
getById: endpoint.get(":id").variables<{ id: string }>().response<User>(),
create: endpoint.post("").variables<{ name: string; email: string }>().response<User>(),
},
})
const api = defineApi({
baseURL: "https://api.example.com",
groups: { users },
})
export { api }Create a caller and use it with full type inference:
import { createCaller } from "@noverax/courier"
import { api } from "@/api/sources"
const caller = createCaller(api)
const users = await caller.users.getAll()
// ^? User[]
const user = await caller.users.getById({ id: "1" })
// ^? UserTwo Transport Modes
Both modes share the same API definition and the same type inference:
- Direct —
createCaller(api)— call backends directly from any runtime (Node.js, Bun, Deno, Cloudflare Workers, server components). - Proxy —
createProxyCaller<typeof api>("api")— browser sends requests through your server, which hides API keys, bypasses CORS, and carries the session cookies you name.
Same definitions, two modes
You define your API once. Both createCaller (direct) and createProxyCaller (proxy) consume the same sources with identical type inference.
Framework Agnostic
The core runs on any JavaScript runtime with fetch. Official adapters handle the server-side proxy for your framework:
| Framework | Server Package | Handler |
|---|---|---|
| Next.js | @noverax/courier-next | apiRoute() |
| Nuxt | @noverax/courier-nuxt | courierHandler() |
| Hono | @noverax/courier-hono | courierHandler() |
| Express | @noverax/courier-express | courierHandler() |
| Fastify | @noverax/courier-fastify | courierPlugin |
| NestJS | @noverax/courier-nestjs | courierMiddleware() |
All adapter handlers are zero-arg — they read sources from the core global registry populated by registerSources() (manual) or attachRuntime() (auto-loads courier.config.ts).
On the client side, use createProxyCaller (vanilla) or createProxyQueryCaller (React Query hooks).
Core Philosophy
- Type safety without codegen — TypeScript functions are the source of truth. No generated files, no build steps.
- Zero boilerplate — define a method signature and Courier handles serialization, path parameters, headers, and error wrapping.
- Any backend — works with Express, FastAPI, Rails, Go, Laravel, Django — anything that speaks HTTP.
- Composable middleware — retry, auth, logging, rate-limiting, caching — or write your own.
- Standard Schema validation — plug in Zod, Valibot, ArkType, or any Standard Schema library.
Comparison
| Feature | fetch | axios | tRPC | Courier |
|---|---|---|---|---|
| Type Safety | Manual | Manual | Full | Full |
| Framework Agnostic | Yes | Yes | Yes | Yes |
| React Query | Manual | Manual | Built-in | Built-in |
| Server Components | Manual | Manual | Supported | First-class |
| Middleware Chain | No | Interceptors | Built-in | Composable |
| Runtime Validation | No | No | Any validator | Any Standard Schema |
| Upload Progress | No | Yes | No | Built-in |
| Backend Requirement | Any | Any | tRPC server | Any REST |
Next Steps
- Installation — add Courier to your project
- Quick Start — make your first type-safe API call
- Framework Guides — setup for your specific framework