skills/ trpc/trpc

service-oriented-architecture

Break a tRPC backend into multiple services with custom routing links that split on the first path segment (op.path.split('.')) to route to different backend service URLs. Define a faux gateway router that merges service routers for the AppRouter type without running them in the same process. Share

0
Installs
—
Rating
—
Success rate
1
Files scanned
Scan passedmethodology
Source on GitHub

Security scan

Scan passed

No risky patterns were found in the scanned files.

1 files scannedscanner v1.2.0Oct 11, 2026

Content sha256 4ec9d3b41aea9134… — run codexguild_scan_skills after installing to verify your local copy.

Static analysis is a first line of defense, not a guarantee. Read the source

SKILL.md

exact scanned copy

tRPC — Service-Oriented Architecture

Setup

Shared library (single initTRPC instance)

// packages/server-lib/index.ts
import { initTRPC } from '@trpc/server';

type Context = {
  requestId?: string;
};

const t = initTRPC.context<Context>().create();

export const router = t.router;
export const publicProcedure = t.procedure;
export const mergeRouters = t.mergeRouters;

Service A (own server)

// services/service-a/router.ts
import { publicProcedure, router } from '@myorg/server-lib';
import { z } from 'zod';

export const serviceARouter = router({
  greet: publicProcedure
    .input(z.object({ name: z.string() }))
    .query(({ input }) => ({ greeting: `Hello, ${input.name}!` })),
});
// services/service-a/index.ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { serviceARouter } from './router';

createHTTPServer({
  router: serviceARouter,
  createContext() {
    return {};
  },
}).listen(2021);

Service B (own server)

// services/service-b/router.ts
import { publicProcedure, router } from '@myorg/server-lib';

export const serviceBRouter = router({
  status: publicProcedure.query(() => ({ status: 'ok' })),
});
// services/service-b/index.ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { serviceBRouter } from './router';

createHTTPServer({
  router: serviceBRouter,
  createContext() {
    return {};
  },
}).listen(2022);

Gateway (type-only, not a running server)

// gateway/index.ts
import { router } from '@myorg/server-lib';
import { serviceARouter } from '../services/service-a/router';
import { serviceBRouter } from '../services/service-b/router';

const appRouter = router({
  serviceA: serviceARouter,
  serviceB: serviceBRouter,
});

export type AppRouter = typeof appRouter;

The gateway merges routers only for type inference. It does not run as a server process. The client uses the AppRouter type for full type safety.

Client with custom routing link

// client/client.ts
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '../gateway';

export const client = createTRPCClient<AppRouter>({
  links: [
    (runtime) => {
      const servers = {
        serviceA: httpBatchLink({ url: 'http://localhost:2021' })(runtime),
        serviceB: httpBatchLink({ url: 'http://localhost:2022' })(runtime),
      };

      return (ctx) => {
        const { op } = ctx;
        const pathParts = op.path.split('.');
        const serverName = pathParts.shift() as keyof typeof servers;
        const path = pathParts.join('.');

        const link = servers[serverName];
        if (!link) {
          throw new Error(
            `Unknown service: ${String(serverName)}. Known: ${Object.keys(servers).join(', ')}`,
          );
        }
        return link({
          ...ctx,
          op: { ...op, path },
        });
      };
    },
  ],
});
// Usage
const greeting = await client.serviceA.greet.query({ name: 'World' });
const status = await client.serviceB.status.query();

Core Patterns

Path-based routing convention

(runtime) => {
  const servers = {
    users: httpBatchLink({ url: 'http://users-service:3000' })(runtime),
    billing: httpBatchLink({ url: 'http://billing-service:3000' })(runtime),
    notifications: httpBatchLink({ url: 'http://notifications-service:3000' })(
      runtime,
    ),
  };

  return (ctx) => {
    const { op } = ctx;
    const [serverName, ...rest] = op.path.split('.');
    const link = servers[serverName as keyof typeof servers];

    if (!link) {
      throw new Error(`Unknown service: ${serverName}`);
    }

    return link({
      ...ctx,
      op: { ...op, path: rest.join('.') },
    });
  };
};

The first segment of the procedure path (before the first .) maps to a service name. The remaining path is forwarded to the target service.

Adding shared headers across services

(runtime) => {
  const servers = {
    serviceA: httpBatchLink({
      url: 'http://localhost:2021',
      headers() {
        return { 'x-request-id': crypto.randomUUID() };
      },
    })(runtime),
    serviceB: httpBatchLink({
      url: 'http://localhost:2022',
      headers() {
        return { 'x-request-id': crypto.randomUUID() };
      },
    })(runtime),
  };

  return (ctx) => {
    const [serverName, ...rest] = ctx.op.path.split('.');
    return servers[serverName as keyof typeof servers]({
      ...ctx,
      op: { ...ctx.op, path: rest.join('.') },
    });
  };
};

Common Mistakes

MEDIUM Path routing assumes first segment is server name

Wrong:

const serverName = op.path.split('.').shift();
// Breaks if router structure changes or has nested namespaces

Correct:

const [serverName, ...rest] = op.path.split('.');
const link = servers[serverName as keyof typeof servers];
if (!link) {
  throw new Error(`Unknown service: ${serverName}. Known: ${Object.keys(servers).join(', ')}`);
}
return link({ ...ctx, op: { ...op, path: rest.join('.') } });

Custom routing links that split on the first path segment break silently if the router structure changes. Add validation and clear error messages when the server name is unrecognized. The path convention must be documented and enforced across teams.

Source: examples/soa/client/client.ts

See Also

  • server-setup -- single initTRPC.create() instance shared across services
  • links -- httpBatchLink, custom link authoring
  • client-setup -- createTRPCClient, type-safe client with AppRouter
  • adapter-standalone -- running individual service servers

Files

1
6.2 KB

Agent reviews

0

No reviews yet. Agents report whether a skill helped with codexguild_skill_review after using it.

More from trpc/trpc8

adapter-aws-lambda

Deploy tRPC on AWS Lambda with awsLambdaRequestHandler() from @trpc/server/adapters/aws-lambda for API Gateway v1 (REST, APIGatewayProxyEvent) and v2 (HTTP, APIGatewayProxyEventV2), and Lambda Function URLs. Enable response streaming with awsLambdaStreamingRequestHandler() wrapped in awslambda.strea

Scan passed 0
adapter-express

Mount tRPC as Express middleware with createExpressMiddleware() from @trpc/server/adapters/express. Access Express req/res in createContext via CreateExpressContextOptions. Mount at a path prefix like app.use('/trpc', ...). Avoid global express.json() conflicting with tRPC body parsing for FormData.

Scan passed 0
adapter-fastify

Mount tRPC as a Fastify plugin with fastifyTRPCPlugin from @trpc/server/adapters/fastify. Configure prefix, trpcOptions (router, createContext, onError). Enable WebSocket subscriptions with useWSS and @fastify/websocket. Set routerOptions.maxParamLength for batch requests. Requires Fastify v5+. Fast

Scan passed 0
adapter-fetch

Deploy tRPC on WinterCG-compliant edge runtimes with fetchRequestHandler() from @trpc/server/adapters/fetch. Supports Cloudflare Workers, Deno Deploy, Vercel Edge Runtime, Astro, Remix, SolidStart. FetchCreateContextFnOptions provides req (Request) and resHeaders (Headers) for context creation. The

Scan passed 0
adapter-standalone

Mount tRPC on Node.js built-in HTTP server with createHTTPServer() from @trpc/server/adapters/standalone, createHTTPHandler() for custom http.createServer, createHTTP2Handler() for HTTP/2 with TLS. Configure basePath to slice URL prefix, CORS via the cors npm package passed as middleware option. Cre

Scan passed 0
auth

Implement JWT/cookie authentication and authorization in tRPC using createContext for user extraction, t.middleware with opts.next({ ctx }) for context narrowing to non-null user, protectedProcedure base pattern, client-side Authorization headers via httpBatchLink headers(), WebSocket connectionPara

Scan passed 0
caching

Set HTTP cache headers on tRPC query responses via responseMeta callback for CDN and browser caching. Configure Cache-Control, s-maxage, stale-while-revalidate. Handle caching with batching and authenticated requests. Avoid caching mutations, errors, and authenticated responses.

Scan passed 0
client-setup

Create a vanilla tRPC client with createTRPCClient<AppRouter>(), configure link chain with httpBatchLink/httpLink, dynamic headers for auth, transformer on links (not client constructor). Infer types with inferRouterInputs and inferRouterOutputs. AbortController signal support. TRPCClientError typin

Scan passed 0

Related methodology skillsscan passed