Files
What you'll learn
- Chain a sub-app's routes so it contributes them to the type of the app that mounts it
- Chain route() calls and export typeof the chain's result, with basePath() in the type too
- Compute ReturnType<typeof hc<AppType>> once in a client module, the guide's compile-before-use pattern
Larger applications
The cats API of Basics lesson 8 was split into one Hono per resource and mounted with app.route(). That split and the RPC types pull in opposite directions: a sub-app registered with app.route('/cats', cats); as a statement is a set of routes the client never sees, and a sub-app whose own routes are statements has an empty type to contribute. The docs' RPC guide and its best-practices page both end on this, "if you want to use RPC features, chain", and this lesson does it for a two-resource API under /api.
Chained sub-apps
A sub-app is chained like any app, and exports itself:
// authors.ts
const app = new Hono()
.get('/', (c) => c.json(authors, 200))
.post('/', (c) => c.json(created, 201))
.get('/:id', (c) => c.json(author, 200));
export default app;
Its paths are relative to wherever it is mounted, '/' for the collection, '/:id' for one item. The mounting app chains its route() calls, and the type it exports is the result of the chain:
// app.ts
const app = new Hono().basePath('/api');
const routes = app.route('/authors', authors).route('/books', books);
export default app;
export type AppType = typeof routes;
route() returns the same app with the sub-app's routes merged into its type under the mount path, so typeof routes knows /api/authors, /api/authors/:id and /api/books, and the client reads client.api.authors[':id'].$get(…). basePath('/api') is in the type too, which is why the client's first segment is api. Whether the runtime export is app or routes does not matter, they are the same object; what matters is that the type comes from the end of the chain. typeof app would be the app before the sub-apps were mounted: a Hono with a base path and no routes, from which hc builds unknown.
Compile the client type once
hc<AppType>() asks TypeScript to walk every route in the app, and a file that calls it in ten places asks ten times. The guide's known issues list the symptom, an editor that slows down as the app grows, and the fix, "compile before use": compute the client's type in one module and export a function that has it.
// client.ts
export type Client = ReturnType<typeof hc<AppType>>;
export const hcWithType = (...args: Parameters<typeof hc>): Client => hc<AppType>(...args);
Callers write hcWithType('https://api.example') and get the same client, typed once. The guide's other suggestions are the same idea from different sides: keep the app and the client in separate files so the client's file is small, and, in a monorepo, keep one version of Hono on both sides, because a mismatch shows up as Type instantiation is excessively deep and possibly infinite.
Your task
data.ts holds two owners and three cats. owners.ts is chained and given; cats.ts is the same code as Basics lesson 8 with a POST added, registered as statements; app.ts mounts both under /api as statements and exports typeof app. client.ts is the compile-once wrapper, and the spec builds its client from it. Nothing compiles yet.
- In
app.ts, chain the tworoute()calls and export the type of the result. - Run: the error moves.
client.apihasownersand nocats. - Chain the routes of
cats.ts. - Run: five tests across both resources, one client.
When it fails
'client' is of type 'unknown':AppTypeis stilltypeof app, the app before anything was mounted.Property 'cats' does not exist on type '{ owners: … }':cats.tsstill registers its routes as statements, so it contributes nothing to the type; or itsroute()call is missing from the chain.Property 'api' does not exist:basePath('/api')was dropped; the client's first segment is the base path.registers a catfails withexpected 400: thePOSTincats.tsno longer refuses an unknown owner, or answers the refusal without its status.
Remember
- Chain each sub-app's routes and export it; chain the
route()calls and exporttypeofthe chain's result. basePath()and mount paths are in the type:client.api.cats[':id'].- Compute
ReturnType<typeof hc<AppType>>once and export a wrapper; keep app and client in separate files. - One Hono version on both sides, or the types collapse.
Stuck? Show a hint
In app.ts: const routes = app.route('/cats', cats).route('/owners', owners); export type AppType = typeof routes. In cats.ts, write the three routes as one chain on new Hono(), as owners.ts does, and keep export default app.
Press Run tests to start the app. Its log appears here.Tests
- client.spec.tsrun to see its tests