Skip to content
tRPC

Service-Oriented Architecture

On this page

A tRPC backend doesn't have to run as one server. You can split your routers across several services, each deployed on its own, and still call them all through one fully typed client.

The setup has four parts:

  • a shared library that creates the one tRPC instance every service uses
  • the services, each with its own router and server
  • a faux gateway, which merges the service routers into one AppRouter type but never runs
  • a client with a custom terminating link that sends each call to the service that owns it

A runnable version is in the SOA example.

1. Share one tRPC instance

Put initTRPC in a library that every service imports. This is what guarantees that the services agree on context, error formatting and transformer, so their routers can be typed together.

server-lib/index.ts
import { const initTRPC: TRPCBuilder<object, object>initTRPC } from '@trpc/server';

type 
type Context = {
    requestId?: string;
}
Context
= {
requestId?: string | undefinedrequestId?: string; }; const
const t: TRPCRootObject<Context, object, RuntimeConfigOptions<Context, object>, {
    ctx: Context;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
= const initTRPC: TRPCBuilder<object, object>initTRPC.TRPCBuilder<object, object>.context<Context>(): TRPCBuilder<Context, object>context<
type Context = {
    requestId?: string;
}
Context
>().
TRPCBuilder<Context, object>.create<RuntimeConfigOptions<Context, object>>(opts?: RuntimeConfigOptions<Context, object> | undefined): TRPCRootObject<Context, object, RuntimeConfigOptions<Context, object>, {
    ctx: Context;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
create
();
export const
const router: RouterBuilder<{
    ctx: Context;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
router
=
const t: TRPCRootObject<Context, object, RuntimeConfigOptions<Context, object>, {
    ctx: Context;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
.
TRPCRootObject<Context, object, RuntimeConfigOptions<Context, object>, { ctx: Context; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.router: RouterBuilder<{
    ctx: Context;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
router
;
export const const publicProcedure: ProcedureBuilder<Context, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>publicProcedure =
const t: TRPCRootObject<Context, object, RuntimeConfigOptions<Context, object>, {
    ctx: Context;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
.TRPCRootObject<Context, object, RuntimeConfigOptions<Context, object>, { ctx: Context; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<Context, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>procedure;

2. Build and serve each service

Each service defines its own router with the shared helpers and runs its own server. Here they use the standalone adapter, but any adapter works.

server-a/router.ts
import { import zz } from 'zod';
import { 
const publicProcedure: ProcedureBuilder<{
    requestId?: string;
}, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>
publicProcedure
,
const router: RouterBuilder<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
router
} from '../server-lib';
export const
const serverARouter: BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greet: QueryProcedure<{
        input: string;
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
serverARouter
=
router<{
    greet: QueryProcedure<{
        input: string;
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>(_: {
    greet: QueryProcedure<{
        input: string;
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}): BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greet: QueryProcedure<{
        input: string;
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
router
({
greet: QueryProcedure<{
    input: string;
    output: {
        greeting: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
}>
greet
:
const publicProcedure: ProcedureBuilder<{
    requestId?: string;
}, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>
publicProcedure
.
ProcedureBuilder<{ requestId?: string; }, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.input<z.ZodString>(schema: z.ZodString): ProcedureBuilder<{
    requestId?: string;
}, object, object, string, string, UnsetMarker, UnsetMarker, false, DefaultErrorShape>
input
(import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string())
.
ProcedureBuilder<{ requestId?: string; }, object, object, string, string, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.query<{
    greeting: string;
}>(resolver: ProcedureResolver<{
    requestId?: string;
}, object, object, string, UnsetMarker, {
    greeting: string;
}>): QueryProcedure<{
    input: string;
    output: {
        greeting: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
}>
query
(({ input: stringinput }) => ({ greeting: stringgreeting: `hello, ${input: stringinput}!` })),
});
server-a/index.ts
import { function createHTTPServer<TRouter extends AnyRouter>(opts: CreateHTTPHandlerOptions<TRouter>): Server<typeof IncomingMessage, typeof ServerResponse>createHTTPServer } from '@trpc/server/adapters/standalone';
import { 
const serverARouter: BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greet: QueryProcedure<{
        input: string;
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
serverARouter
} from './router';
createHTTPServer<BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greet: QueryProcedure<{
        input: string;
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>>(opts: CreateHTTPHandlerOptions<BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greet: QueryProcedure<{
        input: string;
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>>): Server<typeof IncomingMessage, typeof ServerResponse>
createHTTPServer
({
BaseHandlerOptions<BuiltRouter<{ ctx: { requestId?: string; }; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{ greet: QueryProcedure<{ input: string; output: { greeting: string; }; meta: object; errorShape: DefaultErrorShape; }>; }>>, IncomingMessage>.router: BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greet: QueryProcedure<{
        input: string;
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
router
:
const serverARouter: BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greet: QueryProcedure<{
        input: string;
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
serverARouter
,
createContext?: NodeHTTPCreateContextFn<BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greet: QueryProcedure<{
        input: string;
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>, IncomingMessage, ServerResponse<IncomingMessage>> | undefined
createContext
() {
return {}; }, }).Server.listen(port?: number, hostname?: string, backlog?: number, listeningListener?: () => void): Server<typeof IncomingMessage, typeof ServerResponse> (+8 overloads)listen(2021);

server-b looks the same, with its own router, serverBRouter, listening on port 2022.

3. Combine the types in a faux gateway

The gateway merges the service routers under one name each, and exports the result's type. It's never deployed or run: the client only imports its AppRouter type, so none of the services' code ends up in the client.

faux-gateway/index.ts
import { 
const serverARouter: BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greet: QueryProcedure<{
        input: string;
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
serverARouter
} from '../server-a/router';
import {
const serverBRouter: BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    status: QueryProcedure<{
        input: void;
        output: "ok";
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
serverBRouter
} from '../server-b/router';
import {
const router: RouterBuilder<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
router
} from '../server-lib';
const
const appRouter: BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    serverA: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        greet: QueryProcedure<{
            input: string;
            output: {
                greeting: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    serverB: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<...>>;
}>>
appRouter
=
router<{
    serverA: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        greet: QueryProcedure<{
            input: string;
            output: {
                greeting: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    serverB: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        status: QueryProcedure<{
            input: void;
            output: "ok";
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
}>(_: {
    serverA: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        greet: QueryProcedure<{
            input: string;
            output: {
                greeting: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    serverB: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        status: QueryProcedure<{
            input: void;
            output: "ok";
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
}): BuiltRouter<...>
router
({
serverA: BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greet: QueryProcedure<{
        input: string;
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
serverA
:
const serverARouter: BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greet: QueryProcedure<{
        input: string;
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
serverARouter
,
serverB: BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    status: QueryProcedure<{
        input: void;
        output: "ok";
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
serverB
:
const serverBRouter: BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    status: QueryProcedure<{
        input: void;
        output: "ok";
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
serverBRouter
,
}); export type
type AppRouter = Router<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    serverA: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        greet: QueryProcedure<{
            input: string;
            output: {
                greeting: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    serverB: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<...>>;
}>> & DecorateCreateRouterOptions<...>
AppRouter
= typeof
const appRouter: BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    serverA: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        greet: QueryProcedure<{
            input: string;
            output: {
                greeting: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    serverB: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<...>>;
}>>
appRouter
;

4. Route calls to the right service

On the client, a custom link reads the first segment of each procedure path, serverA in serverA.greet, and forwards the call to that service's link with the segment removed, since the service itself only knows greet.

client/client.ts
import { function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>createTRPCClient, function httpBatchLink<TRouter extends AnyRouter>(opts: HTTPBatchLinkOptions<TRouter["_def"]["_config"]["$types"]>): TRPCLink<TRouter>httpBatchLink, type type TRPCLink<TInferrable extends InferrableClientTypes> = (opts: TRPCClientRuntime) => OperationLink<TInferrable>TRPCLink } from '@trpc/client';
import type { 
type AppRouter = Router<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    serverA: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        greet: QueryProcedure<{
            input: string;
            output: {
                greeting: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    serverB: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<...>>;
}>> & DecorateCreateRouterOptions<...>
AppRouter
} from '../faux-gateway';
const
const serviceLink: TRPCLink<BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    serverA: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        greet: QueryProcedure<{
            input: string;
            output: {
                greeting: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    serverB: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<...>>;
}>>>
serviceLink
: type TRPCLink<TInferrable extends InferrableClientTypes> = (opts: TRPCClientRuntime) => OperationLink<TInferrable>TRPCLink<
type AppRouter = Router<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    serverA: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        greet: QueryProcedure<{
            input: string;
            output: {
                greeting: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    serverB: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<...>>;
}>> & DecorateCreateRouterOptions<...>
AppRouter
> = (runtime: TRPCClientRuntimeruntime) => {
// Created once, so calls to the same service are batched together const
const services: {
    serverA: OperationLink<BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        serverA: BuiltRouter<{
            ctx: {
                requestId?: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
            transformer: false;
        }, DecorateCreateRouterOptions<{
            greet: QueryProcedure<{
                input: string;
                output: {
                    greeting: string;
                };
                meta: object;
                errorShape: DefaultErrorShape;
            }>;
        }>>;
        serverB: BuiltRouter<{
            ctx: {
                requestId?: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
            transformer: false;
        }, DecorateCreateRouterOptions<...>>;
    }>>, unknown, unknown>;
    serverB: OperationLink<...>;
}
services
= {
serverA: OperationLink<BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    serverA: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        greet: QueryProcedure<{
            input: string;
            output: {
                greeting: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    serverB: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<...>>;
}>>, unknown, unknown>
serverA
:
httpBatchLink<BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    serverA: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        greet: QueryProcedure<{
            input: string;
            output: {
                greeting: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    serverB: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<...>>;
}>>>(opts: HTTPBatchLinkOptions<...>): TRPCLink<...>
httpBatchLink
<
type AppRouter = Router<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    serverA: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        greet: QueryProcedure<{
            input: string;
            output: {
                greeting: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    serverB: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<...>>;
}>> & DecorateCreateRouterOptions<...>
AppRouter
>({ url: string | URLurl: 'http://localhost:2021' })(runtime: TRPCClientRuntimeruntime),
serverB: OperationLink<BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    serverA: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        greet: QueryProcedure<{
            input: string;
            output: {
                greeting: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    serverB: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<...>>;
}>>, unknown, unknown>
serverB
:
httpBatchLink<BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    serverA: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        greet: QueryProcedure<{
            input: string;
            output: {
                greeting: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    serverB: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<...>>;
}>>>(opts: HTTPBatchLinkOptions<...>): TRPCLink<...>
httpBatchLink
<
type AppRouter = Router<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    serverA: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        greet: QueryProcedure<{
            input: string;
            output: {
                greeting: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    serverB: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<...>>;
}>> & DecorateCreateRouterOptions<...>
AppRouter
>({ url: string | URLurl: 'http://localhost:2022' })(runtime: TRPCClientRuntimeruntime),
}; return (
ctx: {
    op: Operation<unknown>;
    next: (op: Operation<unknown>) => OperationResultObservable<BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        serverA: BuiltRouter<{
            ctx: {
                requestId?: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
            transformer: false;
        }, DecorateCreateRouterOptions<{
            greet: QueryProcedure<{
                input: string;
                output: {
                    greeting: string;
                };
                meta: object;
                errorShape: DefaultErrorShape;
            }>;
        }>>;
        serverB: BuiltRouter<...>;
    }>>, unknown>;
}
ctx
) => {
const [const name: stringname, ...const rest: string[]rest] =
ctx: {
    op: Operation<unknown>;
    next: (op: Operation<unknown>) => OperationResultObservable<BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        serverA: BuiltRouter<{
            ctx: {
                requestId?: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
            transformer: false;
        }, DecorateCreateRouterOptions<{
            greet: QueryProcedure<{
                input: string;
                output: {
                    greeting: string;
                };
                meta: object;
                errorShape: DefaultErrorShape;
            }>;
        }>>;
        serverB: BuiltRouter<...>;
    }>>, unknown>;
}
ctx
.op: Operation<unknown>op.path: stringpath.String.split(separator: string | RegExp, limit?: number): string[] (+1 overload)split('.');
const
const link: OperationLink<BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    serverA: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        greet: QueryProcedure<{
            input: string;
            output: {
                greeting: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    serverB: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<...>>;
}>>, unknown, unknown>
link
=
const services: {
    serverA: OperationLink<BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        serverA: BuiltRouter<{
            ctx: {
                requestId?: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
            transformer: false;
        }, DecorateCreateRouterOptions<{
            greet: QueryProcedure<{
                input: string;
                output: {
                    greeting: string;
                };
                meta: object;
                errorShape: DefaultErrorShape;
            }>;
        }>>;
        serverB: BuiltRouter<{
            ctx: {
                requestId?: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
            transformer: false;
        }, DecorateCreateRouterOptions<...>>;
    }>>, unknown, unknown>;
    serverB: OperationLink<...>;
}
services
[const name: stringname as keyof typeof
const services: {
    serverA: OperationLink<BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        serverA: BuiltRouter<{
            ctx: {
                requestId?: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
            transformer: false;
        }, DecorateCreateRouterOptions<{
            greet: QueryProcedure<{
                input: string;
                output: {
                    greeting: string;
                };
                meta: object;
                errorShape: DefaultErrorShape;
            }>;
        }>>;
        serverB: BuiltRouter<{
            ctx: {
                requestId?: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
            transformer: false;
        }, DecorateCreateRouterOptions<...>>;
    }>>, unknown, unknown>;
    serverB: OperationLink<...>;
}
services
];
if (!
const link: OperationLink<BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    serverA: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        greet: QueryProcedure<{
            input: string;
            output: {
                greeting: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    serverB: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<...>>;
}>>, unknown, unknown>
link
) {
throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
(`No service for procedure "${
ctx: {
    op: Operation<unknown>;
    next: (op: Operation<unknown>) => OperationResultObservable<BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        serverA: BuiltRouter<{
            ctx: {
                requestId?: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
            transformer: false;
        }, DecorateCreateRouterOptions<{
            greet: QueryProcedure<{
                input: string;
                output: {
                    greeting: string;
                };
                meta: object;
                errorShape: DefaultErrorShape;
            }>;
        }>>;
        serverB: BuiltRouter<...>;
    }>>, unknown>;
}
ctx
.op: Operation<unknown>op.path: stringpath}"`);
} return
const link: (opts: {
    op: Operation<unknown>;
    next: (op: Operation<unknown>) => OperationResultObservable<BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        serverA: BuiltRouter<{
            ctx: {
                requestId?: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
            transformer: false;
        }, DecorateCreateRouterOptions<{
            greet: QueryProcedure<{
                input: string;
                output: {
                    greeting: string;
                };
                meta: object;
                errorShape: DefaultErrorShape;
            }>;
        }>>;
        serverB: BuiltRouter<...>;
    }>>, unknown>;
}) => OperationResultObservable<...>
link
({ ...
ctx: {
    op: Operation<unknown>;
    next: (op: Operation<unknown>) => OperationResultObservable<BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        serverA: BuiltRouter<{
            ctx: {
                requestId?: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
            transformer: false;
        }, DecorateCreateRouterOptions<{
            greet: QueryProcedure<{
                input: string;
                output: {
                    greeting: string;
                };
                meta: object;
                errorShape: DefaultErrorShape;
            }>;
        }>>;
        serverB: BuiltRouter<...>;
    }>>, unknown>;
}
ctx
, op: Operation<unknown>op: { ...
ctx: {
    op: Operation<unknown>;
    next: (op: Operation<unknown>) => OperationResultObservable<BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        serverA: BuiltRouter<{
            ctx: {
                requestId?: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
            transformer: false;
        }, DecorateCreateRouterOptions<{
            greet: QueryProcedure<{
                input: string;
                output: {
                    greeting: string;
                };
                meta: object;
                errorShape: DefaultErrorShape;
            }>;
        }>>;
        serverB: BuiltRouter<...>;
    }>>, unknown>;
}
ctx
.op: Operation<unknown>op, path: stringpath: const rest: string[]rest.Array<string>.join(separator?: string): stringjoin('.') } });
}; }; export const
const client: TRPCClient<BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    serverA: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        greet: QueryProcedure<{
            input: string;
            output: {
                greeting: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    serverB: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<...>>;
}>>>
client
=
createTRPCClient<BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    serverA: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        greet: QueryProcedure<{
            input: string;
            output: {
                greeting: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    serverB: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<...>>;
}>>>(opts: CreateTRPCClientOptions<...>): TRPCClient<...>
createTRPCClient
<
type AppRouter = Router<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    serverA: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        greet: QueryProcedure<{
            input: string;
            output: {
                greeting: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    serverB: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<...>>;
}>> & DecorateCreateRouterOptions<...>
AppRouter
>({
links: TRPCLink<BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    serverA: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        greet: QueryProcedure<{
            input: string;
            output: {
                greeting: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    serverB: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<...>>;
}>>>[]
links
: [
const serviceLink: TRPCLink<BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    serverA: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        greet: QueryProcedure<{
            input: string;
            output: {
                greeting: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    serverB: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<...>>;
}>>>
serviceLink
],
});

Each service gets its own link, so each can have its own options, such as headers. Calls are fully typed through the gateway's AppRouter:

client/index.ts
import { 
const client: TRPCClient<BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    serverA: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        greet: QueryProcedure<{
            input: string;
            output: {
                greeting: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    serverB: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<...>>;
}>>>
client
} from './client';
const { greeting } = await
const client: TRPCClient<BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    serverA: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        greet: QueryProcedure<{
            input: string;
            output: {
                greeting: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    serverB: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<...>>;
}>>>
client
.
serverA: DecoratedProcedureRecord<{
    transformer: false;
    errorShape: DefaultErrorShape;
}, DecorateCreateRouterOptions<{
    greet: QueryProcedure<{
        input: string;
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
serverA
.
greet: {
    query: Resolver<{
        input: string;
        output: {
            greeting: string;
        };
        errorShape: DefaultErrorShape;
        transformer: false;
    }>;
}
greet
.
query: (input: string, opts?: TRPCProcedureOptions) => TRPCTaggedPromise<{
    greeting: string;
}, DefaultErrorShape>
query
('tRPC');
const greeting: string
const status = await
const client: TRPCClient<BuiltRouter<{
    ctx: {
        requestId?: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    serverA: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        greet: QueryProcedure<{
            input: string;
            output: {
                greeting: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    serverB: BuiltRouter<{
        ctx: {
            requestId?: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<...>>;
}>>>
client
.
serverB: DecoratedProcedureRecord<{
    transformer: false;
    errorShape: DefaultErrorShape;
}, DecorateCreateRouterOptions<{
    status: QueryProcedure<{
        input: void;
        output: "ok";
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
serverB
.
status: {
    query: Resolver<{
        input: void;
        output: "ok";
        errorShape: DefaultErrorShape;
        transformer: false;
    }>;
}
status
.query: (input: void, opts?: TRPCProcedureOptions) => TRPCTaggedPromise<"ok", DefaultErrorShape>query();
const status: "ok"

Common mistakes

  • Forwarding the full path. If the link doesn't strip the service name, the service is asked for serverA.greet and answers with No procedure found on path "serverA.greet".
  • Creating the service links per call. httpBatchLink(...)(runtime) sets up a new batch queue each time it runs, so create the service links once, in the setup function, as shown above. Otherwise nothing gets batched.
  • Using a separate initTRPC in each service. The gateway's AppRouter can only describe one context, error shape and transformer, so give every service the same instance from the shared library.
  • Leaving the router type off the service links. Links created inside a custom link aren't checked against your router. Pass <AppRouter>, as in httpBatchLink<AppRouter>(...), so TypeScript still requires a transformer when you use one.
  • Running the gateway. The faux gateway only exists for its type. Each service serves its own router, and the client talks to the services directly.