tRPC Proxy Setup
tRPC proxy configuration for server and client integration
Overview
The tRPC proxy setup provides type-safe communication between client and server components. It consists of two main parts:
- Server-side caller (
src/trpc/server.ts) - For Server Components and API routes - Client-side React provider (
src/trpc/react.tsx) - For Client Components with React Query
Both leverage the tRPC router defined in src/trpc/procedures/root.ts to ensure end-to-end type safety.
Server-Side Proxy
The server proxy enables calling tRPC procedures from React Server Components without HTTP overhead.
Configuration (src/trpc/server.ts)
import "server-only";
import { createCaller } from "@/trpc/procedures/root";
import { createTRPCContext } from "@/trpc/procedures/trpc";
import { createHydrationHelpers } from "@trpc/react-query/rsc";
import { cache } from "react";
// Create context with headers and cookies
const createContext = cache(async () => {
const h = new Headers(await headers());
const c = await cookies();
return createTRPCContext({
headers: h,
getCookie: (name: string) => c.get(name)?.value,
setCookie: (name, value, options) => {
// Note: Cookie setting not supported in RSC context
// Use client-side mutations for auth flows
},
});
});
// Create server caller
const caller = createCaller(createContext);
// Export hydration helpers
export const { trpc: api, HydrateClient } = createHydrationHelpers(
caller,
getQueryClient
);Usage in Server Components
import { api } from "@/trpc/server";
export default async function UsersPage() {
// Direct server-side call (no HTTP request)
const users = await api.users.list();
return (
<div>
{users.payload?.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}Prefetching for Client Components
import { api, HydrateClient } from "@/trpc/server";
export default async function Page() {
// Prefetch data in Server Component
await api.users.list.prefetch();
return (
<HydrateClient>
{/* Client component will have instant data */}
<UserList />
</HydrateClient>
);
}The server-side caller cannot set cookies due to RSC limitations. Use client-side mutations for authentication flows that require cookie setting.
Client-Side Proxy
The client proxy integrates tRPC with React Query for reactive data fetching in Client Components.
Configuration (src/trpc/react.tsx)
import { createTRPCReact } from "@trpc/react-query";
import { httpBatchLink } from "@trpc/client";
import SuperJSON from "superjson";
export const api = createTRPCReact<AppRouter>();
export function TRPCReactProvider({ children, headers }) {
const [trpcClient] = useState(() =>
api.createClient({
links: [
httpBatchLink({
transformer: SuperJSON,
url: absoluteUrl(ApiTrpc()),
headers() {
return {
"x-trpc-source": "nextjs-react",
};
},
fetch(url, options) {
return fetch(url, {
...options,
credentials: "include", // Enable cookies
});
},
}),
],
})
);
return (
<QueryClientProvider client={queryClient}>
<api.Provider client={trpcClient} queryClient={queryClient}>
{children}
</api.Provider>
</QueryClientProvider>
);
}Usage in Client Components
"use client";
import { api } from "@/trpc/react";
export function UserList() {
// React Query hook with type safety
const { data, isLoading } = api.users.list.useQuery();
if (isLoading) return <div>Loading...</div>;
return (
<div>
{data?.payload?.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}Mutations
"use client";
import { api } from "@/trpc/react";
export function CreateUserForm() {
const utils = api.useUtils();
const createUser = api.users.create.useMutation({
onSuccess: () => {
// Invalidate queries to refetch data
utils.users.list.invalidate();
},
});
return (
<button onClick={() => createUser.mutate({ name: "John" })}>
Create User
</button>
);
}React Query Integration
The proxy setup includes React Query for powerful data management:
- Automatic caching - Queries are cached by default
- Background refetching - Stale data is refetched automatically
- Optimistic updates - Update UI before server confirms
- Infinite queries - Built-in pagination support
- Cache invalidation - Fine-grained control over refetching
Cache Invalidation Patterns
const utils = api.useUtils();
// Invalidate specific query
utils.users.list.invalidate();
// Invalidate with parameters
utils.users.getById.invalidate({ id: "123" });
// Invalidate all users queries
utils.users.invalidate();
// Refetch immediately
await utils.users.list.refetch();Optimistic Updates
const updateUser = api.users.update.useMutation({
onMutate: async (newData) => {
// Cancel ongoing queries
await utils.users.getById.cancel({ id: newData.id });
// Get current data
const previous = utils.users.getById.getData({ id: newData.id });
// Optimistically update
utils.users.getById.setData({ id: newData.id }, newData);
return { previous };
},
onError: (err, newData, context) => {
// Rollback on error
utils.users.getById.setData(
{ id: newData.id },
context?.previous
);
},
});Middleware Chain
The tRPC proxy respects the middleware chain defined in the router. Each request goes through:
- Context creation - Auth, database, headers
- Rate limiting - API key or user-based limits
- Error handling - Consistent error formatting
- Request logging - Development debugging
For detailed middleware information, see Middleware Architecture.
Type Safety
The proxy maintains full type safety across the stack:
// Server procedure definition
export const userRouter = createTRPCRouter({
getById: publicProcedure
.input(z.object({ id: z.string().uuid() }))
.query(async ({ input }) => {
const user = await db.users.findById(input.id);
return { success: true, payload: user };
}),
});
// Client usage - fully typed!
const { data } = api.users.getById.useQuery({ id: "123" });
// ^? { success: boolean; payload: User | undefined }
// TypeScript error if ID is not UUID
api.users.getById.useQuery({ id: "invalid" }); // ❌ ErrorPerformance Optimization
Batch Requests
The httpBatchLink automatically batches multiple requests:
// These are batched into a single HTTP request
const user = api.users.getById.useQuery({ id: "1" });
const posts = api.posts.list.useQuery();
const comments = api.comments.list.useQuery();Request Deduplication
React Query deduplicates identical requests:
// Only makes one request, even if called multiple times
function Component1() {
api.users.list.useQuery();
}
function Component2() {
api.users.list.useQuery(); // Uses same request
}Error Handling
The proxy automatically handles errors and provides consistent formatting:
const mutation = api.users.create.useMutation({
onError: (error) => {
// error is typed based on your error handling
if (error.data?.code === "UNAUTHORIZED") {
redirect("/login");
}
toast.error(error.message);
},
});Next Steps
- tRPC Routers - Creating tRPC routers
- tRPC Procedures - Defining procedures
- Middleware - Middleware chain details
- Error Handling - Error patterns