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.
Compose route tuples instead of widening the App. Export route files with defineRoute() and register a literal tuple with app.registerRoutes([...]). Chained route() calls also work. Two things deliberately erase inference and collapse the client to a loose 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 on a previously-declared variable instead of returning the chained result or using
registerRoutes().
The modular-monolith guide shows a multi-file composition with route tuples from several bounded contexts. Callback-style group() and plugin register()still provide runtime encapsulation, but their callbacks cannot widen the parent variable's TypeScript generic. Use route tuples as the typed-client composition boundary.
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) | createInProcessClient |
| 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.1spec 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.