install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/trpc-router-composition" ~/.claude/skills/intense-visions-harness-engineering-trpc-router-composition-451c32 && rm -rf "$T"
manifest:
agents/skills/claude-code/trpc-router-composition/SKILL.mdsource content
tRPC: Router Composition
Organize type-safe RPC procedures into nested routers that merge into a single appRouter
When to Use
- Structuring a tRPC API with multiple resource domains (users, posts, comments)
- Splitting router definitions across files to keep each file focused
- Merging routers from different modules without losing end-to-end type safety
- Building a plugin or extension architecture where routers are composed dynamically
Instructions
- Initialize tRPC once per project in
— exportserver/trpc.ts
,t
,router
, andpublicProcedure
from this file.protectedProcedure - Create one router file per resource domain in
— each exports a router created withserver/routers/
.createTRPCRouter() - Merge domain routers into the root
inappRouter
usingserver/root.ts
with each sub-router as a property.createTRPCRouter({ ... }) - Export
type fromAppRouter
— import it in the client to infer all procedure types.server/root.ts - Never create multiple
instances — share the singleinitTRPC
export across all router files.t - Use consistent naming:
,usersRouter
,postsRouter
— the property name becomes the namespace in client calls.commentsRouter - Keep individual routers under ~20 procedures — split by sub-resource or use middleware to group related procedures.
// server/trpc.ts — single tRPC initialization import { initTRPC } from '@trpc/server'; import type { TRPCContext } from './context'; const t = initTRPC.context<TRPCContext>().create({ transformer: superjson, }); export const router = t.router; export const publicProcedure = t.procedure; export const middleware = t.middleware;
// server/routers/posts.ts — domain router import { z } from 'zod'; import { router, publicProcedure, protectedProcedure } from '../trpc'; export const postsRouter = router({ list: publicProcedure .input(z.object({ limit: z.number().min(1).max(100).default(20) })) .query(({ ctx, input }) => ctx.db.post.findMany({ take: input.limit })), create: protectedProcedure .input(z.object({ title: z.string().min(1), content: z.string() })) .mutation(({ ctx, input }) => ctx.db.post.create({ data: { ...input, authorId: ctx.session.user.id } }) ), }); // server/root.ts — merge all routers import { router } from './trpc'; import { postsRouter } from './routers/posts'; import { usersRouter } from './routers/users'; export const appRouter = router({ posts: postsRouter, users: usersRouter, }); export type AppRouter = typeof appRouter;
Details
tRPC uses TypeScript's structural type system to infer the entire API contract from the router definition — no code generation, no schema files, no runtime reflection. The
AppRouter type is the single source of truth for both client and server.
Namespace hierarchy: Nesting routers (
{ posts: postsRouter }) creates a namespace. Client calls become api.posts.list.useQuery(). This mirrors the router file structure and makes API organization visible to consumers.
Single
instance: initTRPC
initTRPC is called once and the resulting t object is shared. Calling it multiple times produces isolated tRPC instances that cannot be merged — a common mistake when trying to split initialization.
vs router()
: mergeRouters()
createTRPCRouter({ posts: postsRouter }) is namespace-based composition — the sub-router's procedures are nested under the key. mergeRouters(routerA, routerB) is flat composition — all procedures from both routers appear at the same level. Use namespacing for domain separation; use mergeRouters sparingly for cross-cutting procedures.
transformer: tRPC procedures serialize/deserialize inputs and outputs as JSON by default. Adding superjson
superjson as a transformer enables passing Date, Map, Set, BigInt, and other non-JSON types through procedures. Add it to both initTRPC on the server and the client link configuration.
Export only
type, not the instance: The AppRouter
appRouter instance contains server-only dependencies (db client, etc.). Export only typeof appRouter (the type) for client consumption — never import the server router directly in client code.
Source
https://trpc.io/docs/server/routers
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
- related_skills: trpc-input-validation, trpc-middleware-pattern, trpc-nextjs-integration, next-route-handlers, api-resource-modeling, api-nested-vs-flat
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.