tRPC v11 Knowledge Patch
Claude's baseline knowledge covers tRPC through v10. This skill provides features from v11 (December 2024) onwards.
Reference Index
- v11 Migration & Breaking Changes — Transformers moved to links, React Query v5 required,
createTRPCProxyClientrenamed, TypeScript 5.7.2+ / Node 18+ requirements - Subscriptions & Streaming — SSE subscriptions via async generators,
tracked()reconnection,httpSubscriptionLink,useSubscriptionstatus union, streaming queries/mutations, embedded promises withhttpBatchStreamLink - TanStack React Query Integration — New
@trpc/tanstack-react-querypackage,createTRPCContext,createTRPCOptionsProxyfor RSC/singletons,.queryOptions()/.mutationOptions()API, migration codemod - Next.js Server Actions —
experimental_callerwithexperimental_nextAppDirCaller, turning procedures into plain async functions, auth guards, progressive enhancement - Advanced Features — Lazy routers,
localLink, shorthand router definitions, non-JSON content types (FormData/File/Blob),@trpc/openapi(alpha), HTTP/2 standalone adapter
Quick Reference — v11 Breaking Changes
| Change | v10 | v11 |
|--------|-----|-----|
| Transformer location | createTRPCClient({ transformer }) | httpBatchLink({ transformer }) |
| React Query version | v4 | v5 required (isPending replaces isLoading) |
| Client constructor | createTRPCProxyClient | createTRPCClient (same API) |
| Subscriptions | Observable + WebSocket | Async generator + SSE |
| Min TypeScript | — | >=5.7.2 |
| Min Node.js | — | 18+ |
Quick Reference — Essential Patterns
Transformer in link (v11)
import { httpBatchLink } from '@trpc/client';
import superjson from 'superjson';
httpBatchLink({
url: '/api/trpc',
transformer: superjson, // moved here from createTRPCClient
});
SSE subscription (async generator)
const appRouter = router({
onEvent: publicProcedure.subscription(async function* (opts) {
for await (const data of on(ee, 'event', { signal: opts.signal })) {
yield data[0];
}
}),
});
SSE config in initTRPC.create():
const t = initTRPC.create({
sse: {
ping: { enabled: true, intervalMs: 15_000 },
client: { reconnectAfterInactivityMs: 20_000 },
},
});
tracked() — subscription reconnection
import { tracked } from '@trpc/server';
t.procedure
.input(z.object({ lastEventId: z.string().nullish() }).optional())
.subscription(async function* (opts) {
if (opts.input?.lastEventId) { /* fetch missed events */ }
for await (const [data] of on(ee, 'add', { signal: opts.signal })) {
yield tracked(data.id, data); // client tracks this id
}
});
httpSubscriptionLink setup
import { splitLink, httpBatchLink, httpSubscriptionLink } from '@trpc/client';
const client = createTRPCClient<AppRouter>({
links: [
splitLink({
condition: (op) => op.type === 'subscription',
true: httpSubscriptionLink({
url: '/api/trpc',
eventSourceOptions: async ({ op }) => ({
headers: { authorization: `Bearer ${token}` },
}),
}),
false: httpBatchLink({ url: '/api/trpc' }),
}),
],
});
useSubscription return type
Returns discriminated union on status: 'idle' | 'connecting' | 'pending' | 'error'.
const sub = trpc.onEvent.useSubscription(undefined, {
onData: (data) => {},
onError: (err) => {},
});
// sub.status, sub.data, sub.error, sub.reset()
Streaming query (httpBatchStreamLink)
const appRouter = router({
stream: publicProcedure.query(async function* () {
for (let i = 0; i < 10; i++) {
yield i;
await new Promise((r) => setTimeout(r, 500));
}
}),
});
// Client: for await (const v of await trpc.stream.query()) { ... }
Embedded promises in streamed responses
publicProcedure.query(() => ({
instant: 'ready',
slow: slowAsyncFn(), // streams when resolved
}));
TanStack React Query options API
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTRPC } from './trpc';
function MyComponent() {
const trpc = useTRPC();
const greeting = useQuery(trpc.greeting.queryOptions({ name: 'Jerry' }));
const create = useMutation(trpc.createUser.mutationOptions());
// Invalidation
const qc = useQueryClient();
await qc.invalidateQueries(trpc.greeting.queryFilter({ name: 'Jerry' }));
}
createTRPCOptionsProxy (RSC / singletons)
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
// RSC: direct router call, no HTTP
const trpc = createTRPCOptionsProxy({ ctx: createTRPCContext, router: appRouter, queryClient: getQueryClient });
// SPA singleton:
const trpc = createTRPCOptionsProxy<AppRouter>({ client: trpcClient, queryClient });
void queryClient.prefetchQuery(trpc.hello.queryOptions({ text: 'world' }));
Migration codemod: npx @trpc/upgrade (select "Migrate Hooks to xxxOptions API").
Next.js server action
'use server';
import { protectedAction } from '../server/trpc';
import { z } from 'zod';
export const createPost = protectedAction
.meta({ span: 'create-post' })
.input(z.object({ title: z.string() }))
.mutation(async (opts) => { /* opts.ctx.user, opts.input.title */ });
// createPost is now: (input: { title: string }) => Promise<void>
Lazy router
import { lazy } from '@trpc/server';
const appRouter = router({
greeting: lazy(() => import('./greeting.js')),
user: lazy(() => import('./user.js').then((m) => m.userRouter)),
});
Non-JSON with batch link (splitLink)
import { splitLink, httpBatchLink, httpLink, isNonJsonSerializable } from '@trpc/client';
createTRPCClient<AppRouter>({
links: [
splitLink({
condition: (op) => isNonJsonSerializable(op.input),
true: httpLink({ url }),
false: httpBatchLink({ url }),
}),
],
});
@trpc/openapi — generate OpenAPI spec
pnpm add @trpc/openapi
pnpm exec trpc-openapi ./src/server/router.ts -o api.json --title "My API" --version 1.0.0