From harness-claude
Integrates tRPC with Next.js App Router via fetch adapter API routes, client providers for React components, and server callers for React Server Components without HTTP round-trips.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Integrate tRPC with Next.js App Router using the fetch adapter, server-side callers, and React Server Components
Builds end-to-end type-safe tRPC APIs with routers, procedures, middleware, subscriptions, and Next.js/React integration for TypeScript full-stack apps.
Integrates tRPC with TanStack Query for type-safe queries, mutations, optimistic updates, and cache invalidation in React apps using api.xxx.useQuery/useMutation.
Provides tRPC v11 knowledge including SSE subscriptions with tracked() reconnection, streaming queries/mutations, TanStack React Query options API, Next.js server actions, lazy routers, OpenAPI support. Load before writing tRPC v11 code.
Share bugs, ideas, or general feedback.
Integrate tRPC with Next.js App Router using the fetch adapter, server-side callers, and React Server Components
generateStaticParams, getServerSideProps equivalents, or route handlersuseQuery) and server components (via direct caller)// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/root';
import { createTRPCContext } from '@/server/context';
import { type NextRequest } from 'next/server';
const handler = (req: NextRequest) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext: () => createTRPCContext({ req }),
});
export { handler as GET, handler as POST };
// lib/trpc/client.tsx
'use client';
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '@/server/root';
export const api = createTRPCReact<AppRouter>();
// lib/trpc/provider.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { useState } from 'react';
import { api } from './client';
import superjson from 'superjson';
function getBaseUrl() {
if (typeof window !== 'undefined') return '';
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
return `http://localhost:${process.env.PORT ?? 3000}`;
}
export function TRPCProvider({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient());
const [trpcClient] = useState(() =>
api.createClient({
links: [
httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
transformer: superjson,
}),
],
})
);
return (
<api.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</api.Provider>
);
}
// lib/trpc/server.ts
import { createCallerFactory } from '@/server/trpc';
import { appRouter } from '@/server/root';
import { createTRPCContext } from '@/server/context';
import { cache } from 'react';
import { headers } from 'next/headers';
// cache() ensures one context per request (React's request-scoped cache)
const createContext = cache(async () => {
const heads = new Headers(await headers());
heads.set('x-trpc-source', 'rsc');
return createTRPCContext({ req: { headers: heads } as Request });
});
const createCaller = createCallerFactory(appRouter);
export const api = createCaller(createContext);
// app/posts/page.tsx — React Server Component (no 'use client')
import { api } from '@/lib/trpc/server';
export default async function PostsPage() {
// Direct procedure call — no HTTP round-trip, no useQuery needed
const posts = await api.post.list({ limit: 20 });
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
// app/posts/NewPostForm.tsx
'use client';
import { api } from '@/lib/trpc/client';
export function NewPostForm() {
const utils = api.useUtils();
const createPost = api.post.create.useMutation({
onSuccess: () => {
// Invalidate the list query to trigger a refetch
void utils.post.list.invalidate();
},
});
return (
<form onSubmit={(e) => {
e.preventDefault();
const form = new FormData(e.currentTarget);
createPost.mutate({
title: form.get('title') as string,
content: form.get('content') as string,
});
}}>
<input name="title" placeholder="Title" />
<textarea name="content" placeholder="Content" />
<button type="submit" disabled={createPost.isPending}>Create</button>
</form>
);
}
// app/layout.tsx
import { TRPCProvider } from '@/lib/trpc/provider';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<TRPCProvider>{children}</TRPCProvider>
</body>
</html>
);
}
Two separate api objects. The server api (from lib/trpc/server.ts) is for Server Components — it calls procedures directly in-process. The client api (from lib/trpc/client.tsx) is for Client Components — it calls procedures over HTTP. Never import the server api in a Client Component (it would bundle server code into the client).
cache() for request-scoped context. React.cache() memoizes the context creation per request in the React Server Component runtime. Without it, every api.xxx() call would create a fresh database connection. With it, all RSC procedure calls share one context (and one DB connection) per request.
createCallerFactory vs direct import. createCallerFactory(appRouter) creates a factory for the server-side caller. This is the stable API — do not call appRouter.createCaller() directly (deprecated in tRPC v11).
Hydration and prefetching. To pre-populate the TanStack Query cache on the server and hydrate it on the client (avoiding a loading flash), use dehydrate/HydrationBoundary from TanStack Query with tRPC's server API. This pattern is optional but eliminates the initial loading state.
httpBatchLink batches multiple queries. When a Client Component calls multiple useQuery hooks, tRPC batches them into a single HTTP request automatically. This is the default behavior with httpBatchLink.
https://trpc.io/docs/client/nextjs