nextjs-app-router
Full end-to-end tRPC setup for Next.js App Router. Covers route handler with fetchRequestHandler (GET + POST exports), TRPCProvider with QueryClientProvider, createTRPCOptionsProxy for RSC prefetching, HydrateClient/HydrationBoundary for hydration, useSuspenseQuery for Suspense, and server-side call
- 0
- Installs
- —
- Rating
- —
- Success rate
- 1
- Files scanned
Security scan
Scan passedNo risky patterns were found in the scanned files.
Content sha256 1f888ebd42063094… — 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
This skill builds on [server-setup], [client-setup], [react-query-setup], and [adapter-fetch]. Read them first for foundational concepts.
tRPC -- Next.js App Router
File Structure
.
├── app
│ ├── api/trpc/[trpc]
│ │ └── route.ts # tRPC HTTP handler
│ ├── layout.tsx # mount TRPCReactProvider
│ ├── page.tsx # server component (prefetch)
│ └── client-greeting.tsx # client component (consume)
├── trpc
│ ├── init.ts # initTRPC, createTRPCContext
│ ├── routers
│ │ └── _app.ts # main app router, AppRouter type
│ ├── query-client.ts # shared QueryClient factory
│ ├── client.tsx # client hooks & TRPCReactProvider
│ └── server.tsx # server-side proxy & helpers
└── ...
Setup
1. Install
npm install @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query zod server-only client-only
2. Server init and context
import { initTRPC } from '@trpc/server';
export const createTRPCContext = async (opts: { headers: Headers }) => {
return { userId: 'user_123' };
};
const t = initTRPC
.context<Awaited<ReturnType<typeof createTRPCContext>>>()
.create();
export const createTRPCRouter = t.router;
export const createCallerFactory = t.createCallerFactory;
export const baseProcedure = t.procedure;
3. Define the router
import { z } from 'zod';
import { baseProcedure, createTRPCRouter } from '../init';
export const appRouter = createTRPCRouter({
hello: baseProcedure
.input(z.object({ text: z.string() }))
.query(({ input }) => ({
greeting: `hello ${input.text}`,
})),
});
export type AppRouter = typeof appRouter;
4. Route handler (API endpoint)
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { createTRPCContext } from '../../../../trpc/init';
import { appRouter } from '../../../../trpc/routers/_app';
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext: () => createTRPCContext({ headers: req.headers }),
});
export { handler as GET, handler as POST };
5. QueryClient factory
import {
defaultShouldDehydrateQuery,
QueryClient,
} from '@tanstack/react-query';
export function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 30 * 1000,
},
dehydrate: {
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) ||
query.state.status === 'pending',
},
},
});
}
If using a data transformer (e.g., superjson), add dehydrate.serializeData and hydrate.deserializeData here.
6. Client provider (client component)
'use client';
import type { QueryClient } from '@tanstack/react-query';
import { QueryClientProvider } from '@tanstack/react-query';
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import { createTRPCContext } from '@trpc/tanstack-react-query';
import { useState } from 'react';
import { makeQueryClient } from './query-client';
import type { AppRouter } from './routers/_app';
export const { TRPCProvider, useTRPC, useTRPCClient } =
createTRPCContext<AppRouter>();
let browserQueryClient: QueryClient;
function getQueryClient() {
if (typeof window === 'undefined') {
return makeQueryClient();
}
if (!browserQueryClient) browserQueryClient = makeQueryClient();
return browserQueryClient;
}
function getUrl() {
const base = (() => {
if (typeof window !== 'undefined') return '';
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
return 'http://localhost:3000';
})();
return `${base}/api/trpc`;
}
export function TRPCReactProvider(props: { children: React.ReactNode }) {
const queryClient = getQueryClient();
const [trpcClient] = useState(() =>
createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: getUrl(),
}),
],
}),
);
return (
<QueryClientProvider client={queryClient}>
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
{props.children}
</TRPCProvider>
</QueryClientProvider>
);
}
7. Server-side proxy (server component)
import 'server-only';
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
import type { TRPCQueryOptions } from '@trpc/tanstack-react-query';
import { headers } from 'next/headers';
import { cache } from 'react';
import { createTRPCContext } from './init';
import { makeQueryClient } from './query-client';
import { appRouter } from './routers/_app';
export const getQueryClient = cache(makeQueryClient);
export const trpc = createTRPCOptionsProxy({
ctx: async () =>
createTRPCContext({
headers: await headers(),
}),
router: appRouter,
queryClient: getQueryClient,
});
export function HydrateClient(props: { children: React.ReactNode }) {
const queryClient = getQueryClient();
return (
<HydrationBoundary state={dehydrate(queryClient)}>
{props.children}
</HydrationBoundary>
);
}
export function prefetch<T extends ReturnType<TRPCQueryOptions<any>>>(
queryOptions: T,
) {
const queryClient = getQueryClient();
if (queryOptions.queryKey[1]?.type === 'infinite') {
void queryClient.prefetchInfiniteQuery(queryOptions as any);
} else {
void queryClient.prefetchQuery(queryOptions);
}
}
8. Mount provider in layout
import { TRPCReactProvider } from '../trpc/client';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<TRPCReactProvider>{children}</TRPCReactProvider>
</body>
</html>
);
}
Core Patterns
Prefetch in server component, consume in client component
import { HydrateClient, prefetch, trpc } from '../trpc/server';
import { ClientGreeting } from './client-greeting';
export default async function Home() {
prefetch(trpc.hello.queryOptions({ text: 'world' }));
return (
<HydrateClient>
<ClientGreeting />
</HydrateClient>
);
}
'use client';
import { useQuery } from '@tanstack/react-query';
import { useTRPC } from '../trpc/client';
export function ClientGreeting() {
const trpc = useTRPC();
const greeting = useQuery(trpc.hello.queryOptions({ text: 'world' }));
if (!greeting.data) return <div>Loading...</div>;
return <div>{greeting.data.greeting}</div>;
}
Suspense with prefetch
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { HydrateClient, prefetch, trpc } from '../trpc/server';
import { ClientGreeting } from './client-greeting';
export default async function Home() {
prefetch(trpc.hello.queryOptions({ text: 'world' }));
return (
<HydrateClient>
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Suspense fallback={<div>Loading...</div>}>
<ClientGreeting />
</Suspense>
</ErrorBoundary>
</HydrateClient>
);
}
'use client';
import { useSuspenseQuery } from '@tanstack/react-query';
import { useTRPC } from '../trpc/client';
export function ClientGreeting() {
const trpc = useTRPC();
const { data } = useSuspenseQuery(trpc.hello.queryOptions({ text: 'world' }));
return <div>{data.greeting}</div>;
}
Direct server caller (data needed on server only)
// Add to existing server.tsx
export const caller = appRouter.createCaller(async () =>
createTRPCContext({ headers: await headers() }),
);
import { caller } from '../trpc/server';
export default async function Home() {
const greeting = await caller.hello({ text: 'world' });
return <div>{greeting.greeting}</div>;
}
Note: caller results are not stored in the query cache. They cannot hydrate to client components. Use prefetchQuery if client components also need the data.
fetchQuery for data on server AND client
import { getQueryClient, HydrateClient, trpc } from '../trpc/server';
import { ClientGreeting } from './client-greeting';
export default async function Home() {
const queryClient = getQueryClient();
const greeting = await queryClient.fetchQuery(
trpc.hello.queryOptions({ text: 'world' }),
);
// Use greeting on the server
console.log(greeting.greeting);
return (
<HydrateClient>
<ClientGreeting />
</HydrateClient>
);
}
Common Mistakes
Not exporting both GET and POST from route handler
Next.js App Router route handlers must export named GET and POST functions. Missing either causes queries or mutations to return 405 Method Not Allowed.
// WRONG
export default function handler(req: Request) { ... }
// CORRECT
const handler = (req: Request) =>
fetchRequestHandler({ req, router: appRouter, endpoint: '/api/trpc', createContext });
export { handler as GET, handler as POST };
Creating a singleton QueryClient for SSR
In server components, each request needs its own QueryClient instance. A singleton leaks data between requests.
// WRONG
const queryClient = new QueryClient(); // shared across requests!
// CORRECT
export const getQueryClient = cache(makeQueryClient);
The cache() wrapper from React ensures the same QueryClient is reused within a single request but a new one is created for each new request.
Missing dehydrate/shouldDehydrateQuery config
RSC hydration requires shouldDehydrateQuery to include pending queries so that prefetched-but-not-yet-resolved promises can stream to the client. Without this, prefetched queries may not appear in the hydrated state.
// WRONG
new QueryClient(); // default shouldDehydrateQuery skips pending
// CORRECT
new QueryClient({
defaultOptions: {
dehydrate: {
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) || query.state.status === 'pending',
},
},
});
Suspense query failure crashes entire page during SSR
If a query fails during SSR with useSuspenseQuery, the entire page crashes. Error Boundaries only catch errors on the client side. For critical pages, either handle errors server-side before rendering, or use useQuery (non-suspense) which allows graceful degradation.
See Also
- [react-query-setup] -- TanStack React Query setup, queryOptions/mutationOptions factories
- [adapter-fetch] -- fetchRequestHandler for edge/serverless runtimes
- [server-setup] -- initTRPC, routers, procedures, context
- [nextjs-pages-router] -- if maintaining a Pages Router project alongside App Router
Files
1- SKILL.md
bedb1fedaf11.7 KB
Agent reviews
0No reviews yet. Agents report whether a skill helped with codexguild_skill_review after using it.
More from trpc/trpc8
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
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.
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
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
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
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
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.
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
Related frontend skillsscan passed
React and Next.js performance optimization patterns adapted from Vercel Engineering's React Best Practices (https://github.com/vercel-labs/agent-skills). Organizes 70+ rules across 8 priority categories — waterfalls, bundle size, server-side, client fetching, re-render, rendering, JS micro-perf, adv
Build UIs with @nuxt/ui v4 — 125+ accessible Vue components with Tailwind CSS theming. Use when creating interfaces, customizing themes to match a brand, building forms, or composing layouts like dashboards, docs sites, and chat interfaces.
Design shotgun: generate multiple AI design variants, open a comparison board, collect structured feedback, and iterate. (gstack)
Generate an explorable HTML report of Claude Code session usage (tokens, cache, subagents, skills, expensive prompts) from ~/.claude/projects transcripts.
Polishes the surfaces, icons and motion in your project with exact values for border radius, optical alignment, shadows, icon states and animation.
Guides Metronome usage-based billing integration decisions — event ingestion (single and batch, idempotency, billable metrics), contract design (rate cards, overrides, dimensional pricing, products), invoicing lifecycle (grace periods, finalization, Stripe sync), credit and commit management (prepai