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
AppRoutertype 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.
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.
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}!` })),
});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.
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.
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:
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 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();Common mistakes
- Forwarding the full path. If the link doesn't strip the service name, the service is asked for
serverA.greetand answers withNo 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
initTRPCin each service. The gateway'sAppRoutercan 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 inhttpBatchLink<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.