Typed clients
DaloyJS ships two ways to call your API with full type-safety. Use whichever fits your consumer.
1. In-process typed client (zero codegen)
For TypeScript consumers in the same monorepo (tests, internal tools, Next.js server actions):
The client is keyed by operationId, returns a discriminated union of {status, body, headers}, and infers everything from the route definition itself. No build step.
Inference requires method chaining. Each app.route(...) call returns a new App type that accumulates the route, so chain your registrations (new App().route(a).route(b)) and let TypeScript infer the variable's type. Two things erase inference and collapse the client back to a loose, untyped surface:
- Annotating the instance with a bare
const app: App = ...(or returning: Appfrom a factory). The widening annotation discards the accumulated routes, so let the type be inferred instead. - Registering routes as separate statements (
app.route(a); app.route(b);) on a previously-declared variable. The variable keeps its original type, so the new routes never reach the client.
If you import app from another module, export it without a widening annotation (export const app = new App().route(...)...;) so its inferred type, and the typed client, crosses the module boundary intact.
2. Hey API SDK (cross-language, cross-repo, build-time)
For consumers outside the monorepo or in other languages, generate a fully typed fetch SDK with @hey-api/openapi-ts.
- 01Routesapp.route(...)
- 02generateOpenAPI@daloyjs/core/openapi
- 03openapi.jsonOpenAPI 3.1 spec on disk
- 04openapi-tsHey API generator
- 05Typed SDKsdk.gen.ts · types.gen.ts
- 06Consumerfully typed fetch calls
Using the generated SDK
Which one should I use?
| Use case | Pick |
|---|---|
| Same-repo TypeScript caller (tests, internal tools) | In-process createClient |
| Web app / mobile RN bundle in a separate repo | Hey API SDK |
| Non-TypeScript consumer (Python, Swift, Kotlin) | OpenAPI doc + their preferred generator |
| Public SDK for third parties | Hey API SDK, published as its own package |
Coming from ts-rest?
ts-rest is a popular contract-first library that gives you end-to-end TypeScript types without codegen by sharing a contract (initContract) between an adapter-based server (Express, Fastify, NestJS, Next.js) and a fetch client (initClient). If you like that model, DaloyJS will feel familiar, with two differences.
First, in DaloyJS the route definition is the contract, there is no separate contract object to keep in sync. The in-process createClient shown above gives the same zero-codegen, shared-types experience for same-repo TypeScript callers.
Second, ts-rest's type safety is TypeScript-only and requires the client to import the contract. DaloyJS emits a first-class OpenAPI 3.1 spec and a Hey API SDK from the same routes, so consumers that can't import your types (other repos, other languages, public SDKs) are covered too. In ts-rest, OpenAPI is an optional add-on (@ts-rest/open-api). DaloyJS is also the server and runtime itself, portable across Node, Bun, Deno, Cloudflare, and Vercel, rather than a typing layer mounted on a separate framework.
| ts-rest | DaloyJS | |
|---|---|---|
| Contract source | Separate initContract object | The route definition itself |
| Zero-codegen typed client | Yes (initClient, TypeScript only) | Yes (createClient, TypeScript only) |
| Cross-language / cross-repo clients | OpenAPI add-on (@ts-rest/open-api) | OpenAPI 3.1 + Hey API SDK, first-class |
| Server | Adapter on Express / Fastify / NestJS / Next.js | Built-in, runtime-portable |
| Runtime validation | Standard Schema (Zod / Valibot / ArkType) | Standard Schema (Zod / Valibot / ArkType / TypeBox) |
| Security defaults | Bring your own | Built-in headers, CSRF, rate limits, body limits, and more |
Need a bigger contract to validate your generator output? Use the large fake REST demo as the stress case instead of a minimal tutorial app.