@routegraph/client
v1.0.0
Published
Type-safe fetch client generated from your RouteGraph routes, no codegen step.
Readme
@routegraph/client
A type-safe fetch client for RouteGraph APIs — types come from a generated RouteMap type, not from a runtime schema registry. No response validation at runtime; the type contract is compile-time only.
Installation
pnpm add @routegraph/clientNo peer dependencies — @routegraph/core is referenced for types only, never imported at runtime.
createClient<TRouteMap>(options)
function createClient<TRouteMap extends RouteMap>(options: ClientOptions): ClientProxy<TRouteMap>interface ClientOptions {
baseUrl: string
headers?: Record<string, string>
fetch?: typeof fetch // inject a custom fetch, e.g. for testing
onError?: (err: ClientError) => void
timeout?: number // ms; default 30000, applied via AbortSignal.timeout()
}createClient returns a nested Proxy: api['/users/:id'].GET(args) resolves through two get traps into callRoute(options, 'GET', '/users/:id', args) — no per-route code is generated ahead of time.
ClientResponse<T>
interface ClientResponse<T> {
data: T
status: number
headers: Record<string, string>
ok: boolean
raw: Response // the underlying fetch Response
}ClientError
Thrown (and passed to options.onError first) on any non-2xx response:
class ClientError extends Error {
status: number
body: unknown // the parsed response body
request: { method: string; url: string }
}Usage
import { createClient } from '@routegraph/client'
import type { AppRouteMap } from './routemap.js' // generated by `routegraph generate-client`
const api = createClient<AppRouteMap>({
baseUrl: 'http://localhost:3000/api',
headers: { 'x-demo': 'true' },
onError: (err) => console.error('[client]', err.status, err.body),
})
const users = await api['/users'].GET({ query: { role: 'admin' } })
console.log(users.data) // typed from the route's response schema
const created = await api['/users'].POST({ body: { name: 'Ada', email: '[email protected]' } })
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// TS error if the route's body schema requires
// a field this object is missing
const user = await api['/users/:id'].GET({ params: { id: created.data.id } })How to use with generate-client
routegraph generate-client --dir ./routes --out ./routemap.tsThis writes a routemap.ts containing only the AppRouteMap type (no runtime code) — built by walking your loaded routes and rendering each one's Zod request/response schemas into a TypeScript type string. Re-run it whenever your routes change (or use --watch); AppRouteMap is what you pass to createClient<AppRouteMap>(). There is no codegen step for the client's runtime behavior itself — createClient's Proxy-based dispatch is a normal, hand-written, published implementation that works for any RouteMap-shaped type you give it.
callRoute() for manual use
import { callRoute } from '@routegraph/client'
const res = await callRoute<{ status: 'ok' }>(
{ baseUrl: 'http://localhost:3000/api' },
'GET',
'/health'
)Useful if you want the URL-building/error-handling behavior without the Proxy ergonomics, or need to call a route not present in your RouteMap.
How AbortSignal/timeout works
If you don't pass args.signal, callRoute builds one via AbortSignal.timeout(options.timeout ?? 30000) — the request aborts automatically after that many milliseconds. Pass your own signal in RequestArgs to control cancellation yourself (the injected timeout signal is only used as a fallback, not combined with a custom one).
