Skip to main content
Version: 10.x

Merging Routers

Writing all API-code in your code in the same file is not a great idea. It's easy to merge routers with other routers.

Merging with child routers

server.ts
ts
// @filename: trpc.ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
 
 
export const middleware = t.middleware;
export const router = t.router;
export const publicProcedure = t.procedure;
 
// @filename: routers/_app.ts
import { router } from '../trpc';
import { z } from 'zod';
 
import { userRouter } from './user';
import { postRouter } from './post';
 
const appRouter = router({
user: userRouter, // put procedures under "user" namespace
post: postRouter, // put procedures under "post" namespace
});
 
export type AppRouter = typeof appRouter;
 
 
// @filename: routers/post.ts
import { router, publicProcedure } from '../trpc';
import { z } from 'zod';
export const postRouter = router({
create: publicProcedure
.input(
z.object({
title: z.string(),
}),
)
.mutation(({ input }) => {
(parameter) input: { title: string; }
// [...]
}),
list: publicProcedure.query(() => {
// ...
return [];
}),
});
 
// @filename: routers/user.ts
import { router, publicProcedure } from '../trpc';
import { z } from 'zod';
export const userRouter = router({
list: publicProcedure.query(() => {
// [..]
return [];
}),
});
 
server.ts
ts
// @filename: trpc.ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
 
 
export const middleware = t.middleware;
export const router = t.router;
export const publicProcedure = t.procedure;
 
// @filename: routers/_app.ts
import { router } from '../trpc';
import { z } from 'zod';
 
import { userRouter } from './user';
import { postRouter } from './post';
 
const appRouter = router({
user: userRouter, // put procedures under "user" namespace
post: postRouter, // put procedures under "post" namespace
});
 
export type AppRouter = typeof appRouter;
 
 
// @filename: routers/post.ts
import { router, publicProcedure } from '../trpc';
import { z } from 'zod';
export const postRouter = router({
create: publicProcedure
.input(
z.object({
title: z.string(),
}),
)
.mutation(({ input }) => {
(parameter) input: { title: string; }
// [...]
}),
list: publicProcedure.query(() => {
// ...
return [];
}),
});
 
// @filename: routers/user.ts
import { router, publicProcedure } from '../trpc';
import { z } from 'zod';
export const userRouter = router({
list: publicProcedure.query(() => {
// [..]
return [];
}),
});
 

Defining an inline sub-router

When you define an inline sub-router, you can represent your router as a plain object.

In the below example, nested1 and nested2 are equal:

server/_app.ts
ts
import * as trpc from '@trpc/server';
import { publicProcedure, router } from './trpc';
 
const appRouter = router({
// Shorthand plain object for creating a sub-router
nested1: {
proc: publicProcedure.query(() => '...'),
},
//
nested2: router({
proc : publicProcedure.query(() => '...'),
}),
});
server/_app.ts
ts
import * as trpc from '@trpc/server';
import { publicProcedure, router } from './trpc';
 
const appRouter = router({
// Shorthand plain object for creating a sub-router
nested1: {
proc: publicProcedure.query(() => '...'),
},
//
nested2: router({
proc : publicProcedure.query(() => '...'),
}),
});
caution

We recommend you to only define inline sub-routers within a file, and to keep the exported routers as a t.router object. This makes any potential type errors show up in the file they originate from, and not at the place where you merge them.

See a deep dive here

When defining a router as a plain object, any keys are valid. This means you can define a router like this:

routers/user.ts
ts
export const userRouter = {
nested: {
notAProcedure: () => 'Hello world', // <-- actual error here
},
};
routers/user.ts
ts
export const userRouter = {
nested: {
notAProcedure: () => 'Hello world', // <-- actual error here
},
};

without any errors being shown. But when you try to merge this router somewhere else, things will blow up:

routers/user.ts
ts
export const appRouter = router({
user: userRouter, // <-- ❌ error displayed here
Type '{ nested: { notAProcedure: () => string; }; }' is not assignable to type 'ProcedureRouterRecord | AnyProcedure | AnyRouter'. Type '{ nested: { notAProcedure: () => string; }; }' is not assignable to type 'ProcedureRouterRecord'. Property 'nested' is incompatible with index signature. Type '{ notAProcedure: () => string; }' is not assignable to type 'ProcedureRouterRecord | AnyProcedure | AnyRouter'. Type '{ notAProcedure: () => string; }' is not assignable to type 'ProcedureRouterRecord'. Property 'notAProcedure' is incompatible with index signature. Type '() => string' is not assignable to type 'ProcedureRouterRecord | AnyProcedure | AnyRouter'. Type '() => string' is missing the following properties from type 'Procedure<"query" | "mutation" | "subscription", any>': _type, _def, _procedure2322Type '{ nested: { notAProcedure: () => string; }; }' is not assignable to type 'ProcedureRouterRecord | AnyProcedure | AnyRouter'. Type '{ nested: { notAProcedure: () => string; }; }' is not assignable to type 'ProcedureRouterRecord'. Property 'nested' is incompatible with index signature. Type '{ notAProcedure: () => string; }' is not assignable to type 'ProcedureRouterRecord | AnyProcedure | AnyRouter'. Type '{ notAProcedure: () => string; }' is not assignable to type 'ProcedureRouterRecord'. Property 'notAProcedure' is incompatible with index signature. Type '() => string' is not assignable to type 'ProcedureRouterRecord | AnyProcedure | AnyRouter'. Type '() => string' is missing the following properties from type 'Procedure<"query" | "mutation" | "subscription", any>': _type, _def, _procedure
});
routers/user.ts
ts
export const appRouter = router({
user: userRouter, // <-- ❌ error displayed here
Type '{ nested: { notAProcedure: () => string; }; }' is not assignable to type 'ProcedureRouterRecord | AnyProcedure | AnyRouter'. Type '{ nested: { notAProcedure: () => string; }; }' is not assignable to type 'ProcedureRouterRecord'. Property 'nested' is incompatible with index signature. Type '{ notAProcedure: () => string; }' is not assignable to type 'ProcedureRouterRecord | AnyProcedure | AnyRouter'. Type '{ notAProcedure: () => string; }' is not assignable to type 'ProcedureRouterRecord'. Property 'notAProcedure' is incompatible with index signature. Type '() => string' is not assignable to type 'ProcedureRouterRecord | AnyProcedure | AnyRouter'. Type '() => string' is missing the following properties from type 'Procedure<"query" | "mutation" | "subscription", any>': _type, _def, _procedure2322Type '{ nested: { notAProcedure: () => string; }; }' is not assignable to type 'ProcedureRouterRecord | AnyProcedure | AnyRouter'. Type '{ nested: { notAProcedure: () => string; }; }' is not assignable to type 'ProcedureRouterRecord'. Property 'nested' is incompatible with index signature. Type '{ notAProcedure: () => string; }' is not assignable to type 'ProcedureRouterRecord | AnyProcedure | AnyRouter'. Type '{ notAProcedure: () => string; }' is not assignable to type 'ProcedureRouterRecord'. Property 'notAProcedure' is incompatible with index signature. Type '() => string' is not assignable to type 'ProcedureRouterRecord | AnyProcedure | AnyRouter'. Type '() => string' is missing the following properties from type 'Procedure<"query" | "mutation" | "subscription", any>': _type, _def, _procedure
});

This can be very confusing, and if your routers are big with lots of procedures, the error message will be impossible to comprehend. To fix this, only use inline sub-routers within a file, and keep the exported routers as a t.router object. This way, the error will show up in the file where the error originates from, and not at the place where you merge them:

routers/user.ts
ts
export const userRouter = router({
nested: {
notAProcedure: () => 'Hello world', // <-- ✅ error displayed where it originates
Type '() => string' is not assignable to type 'ProcedureRouterRecord | AnyProcedure | AnyRouter'. Type '() => string' is missing the following properties from type 'Procedure<"query" | "mutation" | "subscription", any>': _type, _def, _procedure2322Type '() => string' is not assignable to type 'ProcedureRouterRecord | AnyProcedure | AnyRouter'. Type '() => string' is missing the following properties from type 'Procedure<"query" | "mutation" | "subscription", any>': _type, _def, _procedure
},
});
routers/user.ts
ts
export const userRouter = router({
nested: {
notAProcedure: () => 'Hello world', // <-- ✅ error displayed where it originates
Type '() => string' is not assignable to type 'ProcedureRouterRecord | AnyProcedure | AnyRouter'. Type '() => string' is missing the following properties from type 'Procedure<"query" | "mutation" | "subscription", any>': _type, _def, _procedure2322Type '() => string' is not assignable to type 'ProcedureRouterRecord | AnyProcedure | AnyRouter'. Type '() => string' is missing the following properties from type 'Procedure<"query" | "mutation" | "subscription", any>': _type, _def, _procedure
},
});

routers/_app.ts
ts
export const appRouter = router({
user: userRouter,
});
routers/_app.ts
ts
export const appRouter = router({
user: userRouter,
});

Merging with t.mergeRouters

If you prefer having all procedures flat in one single namespace, you can instead use t.mergeRouters

server.ts
ts
// @filename: routers/_app.ts
import { router, publicProcedure, mergeRouters } from '../trpc';
import { z } from 'zod';
 
import { userRouter } from './user';
import { postRouter } from './post';
 
const appRouter = mergeRouters(userRouter, postRouter)
 
export type AppRouter = typeof appRouter;
 
// @filename: routers/post.ts
import { router, publicProcedure } from '../trpc';
import { z } from 'zod';
export const postRouter = router({
postCreate: publicProcedure
.input(
z.object({
title: z.string(),
}),
)
.mutation(({ input }) => {
(parameter) input: { title: string; }
// [...]
}),
postList: publicProcedure.query(() => {
// ...
return [];
}),
});
 
 
// @filename: routers/user.ts
import { router, publicProcedure } from '../trpc';
import { z } from 'zod';
export const userRouter = router({
userList: publicProcedure.query(() => {
// [..]
return [];
}),
});
 
server.ts
ts
// @filename: routers/_app.ts
import { router, publicProcedure, mergeRouters } from '../trpc';
import { z } from 'zod';
 
import { userRouter } from './user';
import { postRouter } from './post';
 
const appRouter = mergeRouters(userRouter, postRouter)
 
export type AppRouter = typeof appRouter;
 
// @filename: routers/post.ts
import { router, publicProcedure } from '../trpc';
import { z } from 'zod';
export const postRouter = router({
postCreate: publicProcedure
.input(
z.object({
title: z.string(),
}),
)
.mutation(({ input }) => {
(parameter) input: { title: string; }
// [...]
}),
postList: publicProcedure.query(() => {
// ...
return [];
}),
});
 
 
// @filename: routers/user.ts
import { router, publicProcedure } from '../trpc';
import { z } from 'zod';
export const userRouter = router({
userList: publicProcedure.query(() => {
// [..]
return [];
}),
});