# Large routers

> Keep type-checking fast with thousands of procedures, by building the server's declarations and typing apps from them.

A client's types come from the router's type. If the client reads the server's source, TypeScript infers every procedure (input schemas, middleware, what each resolver returns) before it checks a single call. If it reads the server's built declarations (`.d.ts`), that work is already done and the client pays only for the procedures it calls.

Measured on a generated monorepo with 5000 zod procedures in 50 domain packages, a client app calling 200 of them:

| The app reads                   | Instantiations | Check |
| ------------------------------- | -------------: | ----: |
| the server's source             |      4,136,629 | 6.51s |
| the server's `.d.ts`            |         42,852 | 0.07s |
| two domains' source             |        253,470 | 0.29s |
| two domains' contracts (source) |        205,566 | 0.22s |

From declarations, the cost doesn't grow with the router. [`spikes/big-routers`](https://github.com/trpc/trpc/blob/v12/next/spikes/big-routers/generate.ts) generates that monorepo (`node big-routers/bench.ts 50 100` from `spikes/`), and [`examples/monorepo`](https://github.com/trpc/trpc/blob/v12/next/examples/monorepo/README.md) is a small one laid out the way this page describes, which `pnpm example` type-checks and runs.

## Build the server's declarations

Make each server package a TypeScript project that emits declarations, and have apps reference it:

```json
{
  "compilerOptions": {
    "composite": true,
    "emitDeclarationOnly": true,
    "declarationMap": true,
    "rootDir": "src",
    "outDir": "dist"
  },
  "references": [{ "path": "../trpc" }]
}
```

An app's `tsconfig.json` lists the api package in `references`, and imports its router type as usual (`import type { AppRouter } from '@acme/api'`). `tsc -b` builds the referenced packages first, then checks the app against their `dist/*.d.ts` instead of their source, even when the package exports its source files.

In the example, every package extends [`tsconfig.base.json`](https://github.com/trpc/trpc/blob/v12/next/examples/monorepo/tsconfig.base.json) and lists what it imports, as [`packages/posts`](https://github.com/trpc/trpc/blob/v12/next/examples/monorepo/packages/posts/tsconfig.json) and [`apps/web`](https://github.com/trpc/trpc/blob/v12/next/examples/monorepo/apps/web/tsconfig.json) do. The root [`tsconfig.json`](https://github.com/trpc/trpc/blob/v12/next/examples/monorepo/tsconfig.json) references them all, so one `tsc -b` builds and checks the repo.

Split the server by domain (a package per area, each exporting its router and its type, like [`packages/posts`](https://github.com/trpc/trpc/blob/v12/next/examples/monorepo/packages/posts/src/index.ts)) so `tsc -b` rebuilds only the packages a change reaches. Keep `t` and base procedures such as `authed` in a package of their own that every domain depends on ([`packages/trpc`](https://github.com/trpc/trpc/blob/v12/next/examples/monorepo/packages/trpc/src/index.ts)).

## Name the root router's type

Mount the domains in a router whose type you write from theirs:

```ts
export type AppRouter = {
  posts: PostsRouter;
  users: UsersRouter;
};

export const appRouter: AppRouter = {
  posts: postsRouter,
  users: usersRouter,
};
```

The declarations then refer to each domain's type. Inferred from the object instead, they copy every domain's procedures into the api package's `.d.ts`, and past a few thousand procedures TypeScript stops with TS7056 ("the inferred type of this node exceeds the maximum length the compiler will serialize"). Write it as a `type`: an `interface` isn't accepted as a router. The example's [`packages/api`](https://github.com/trpc/trpc/blob/v12/next/examples/monorepo/packages/api/src/index.ts) mounts its three domains this way.

## One client or several

With declarations, one client over the whole `AppRouter` is as cheap as a client of the domains an app uses: the client's types resolve a procedure only when a call reads it, so `Pick<AppRouter, 'posts'>` or a client per domain gains nothing. The example's [`apps/web`](https://github.com/trpc/trpc/blob/v12/next/examples/monorepo/apps/web/src/index.ts) uses one client over the whole API.

Smaller clients help when the app can't read declarations (no build step before type-checking). A client of `{ posts: PostsRouter }` imported from the posts package loads only that domain, about 16× less than the whole router in the measurement.

## Contracts for apps that need one domain

An app that uses one domain can depend on a [contract](https://v12.trpc.io/docs/server/contract-first.md) package instead of the server. It loads no server code (resolvers, database clients, their types), and its calls can be validated against the contract's schemas on the client. Build the contract package's declarations too; its cost is then its schemas'.

In the example, [`packages/billing-contract`](https://github.com/trpc/trpc/blob/v12/next/examples/monorepo/packages/billing-contract/src/index.ts) is the contract, [`packages/billing`](https://github.com/trpc/trpc/blob/v12/next/examples/monorepo/packages/billing/src/index.ts) implements it with `t.implement()`, and [`apps/billing-widget`](https://github.com/trpc/trpc/blob/v12/next/examples/monorepo/apps/billing-widget/src/index.ts) calls it with `validationLink`, depending on the contract package alone.

A server that isn't written contract-first can still hand clients a contract: `toContract(router)` derives one, as [`examples/openapi`](https://github.com/trpc/trpc/blob/v12/next/examples/openapi/index.ts) does for its `openApiLink` client. Its types come from the router's, so the app still needs the server's declarations to stay cheap.

## In the editor

Editors follow project references to the referenced package's source, so the editor re-infers the server even when `tsc -b` doesn't. To have the editor read declarations too, set `disableSourceOfProjectReferenceRedirect: true` in the app's `tsconfig.json` and keep `tsc -b --watch` running: changes to the server show in the app once rebuilt. With `declarationMap: true` in the server packages, go-to-definition still lands in their source.

## Measuring

`tsc -p apps/web --extendedDiagnostics` prints the instantiation count and check time of one project; `--listFiles` shows whether it loaded a package's `src/` or its `dist/`. `tsc --generateTrace <dir>` records which types took the time.
