tRPC + Next.js: The Ultimate Solution for Type Mismatch Between Client and Server

Development tutorial - IT technology blog
Development tutorial - IT technology blog

The Nightmare of ‘Data Mismatch’

Have you ever stayed up until 2 AM just to track down an undefined error? It’s a familiar scenario: the Backend silently renames a field from user_id to userId. The Frontend continues to use the old name. TypeScript is completely “blind” at this point because it has no idea what happened on the Server. The result? The app crashes right in the customer’s hands.

The core issue lies in the generational gap between Client and Server. Whether you use REST or GraphQL, you still have to redefine interfaces on both ends. This is time-consuming and extremely error-prone. In fact, I once spent three hours just fixing a typo in a Swagger file. Technology should be doing this work for us.

That’s why tRPC is a lifesaver. It directly connects types from the Backend to the Frontend without an intermediate step. When you modify Server code, VS Code on the Client side will immediately flag it in red. No need to wait until the app runs to realize you’ve made a mistake.

What is tRPC and Why is it Different?

tRPC (TypeScript Remote Procedure Call) isn’t some magical framework. It’s simply a smart data transport layer. The biggest selling point of tRPC is that it requires no Schema (like .graphql) and no Code Generation.

It takes full advantage of TypeScript’s inference mechanism. When you write a function on the Server, the Client automatically knows what parameters that function needs and what object it returns. Everything happens in real-time.

Pro-tip: When working with complex API objects, I often use toolcraft.app/en/tools/developer/json-formatter. This tool helps format data extremely quickly to check the structure before mapping it into logic. It’s much more convenient than installing heavy extensions.

Installing tRPC for a Next.js Project

I’m assuming you’re using the Next.js App Router with TypeScript. If you’re starting fresh, use npx create-next-app@latest. Next, install the core tRPC libraries and Zod for data validation:

npm install @trpc/client @trpc/server @trpc/react-query @trpc/next @tanstack/react-query zod

Zod will act as the “gatekeeper.” It validates input data and helps tRPC understand the structure the Client needs to send.

Server-side Setup (Backend)

First, create the tRPC initialization file. I usually place it at src/server/trpc.ts.

1. Initialize the tRPC Instance

import { initTRPC } from '@trpc/server';

const t = initTRPC.create();

export const router = t.router;
export const publicProcedure = t.procedure;

2. Define Procedures (API Endpoints)

Instead of writing traditional /api/users routes, you define them in the Router. Create the file src/server/routers/_app.ts:

import { z } from 'zod';
import { router, publicProcedure } from '../trpc';

export const appRouter = router({
  getUser: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async (opts) => {
      const { input } = opts;
      return {
        id: input.id,
        name: 'John Doe',
        email: '[email protected]',
      };
    }),
});

export type AppRouter = typeof appRouter;

Notice the export type AppRouter line. This is the “magic.” The Client only imports this Type, never the processing logic. This keeps the Frontend bundle size lightweight.

3. Create the Route Handler

In the App Router, you need a handler to catch requests at src/app/api/trpc/[trpc]/route.ts:

import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/routers/_app';

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: '/api/trpc',
    req,
    router: appRouter,
    createContext: () => ({}),
  });

export { handler as GET, handler as POST };

Connecting the Client (Frontend)

Now it’s time to reap the rewards. You need to create a utility so tRPC knows how to call the Server.

1. Create React Hooks

Create the file src/utils/trpc.ts:

import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '@/server/routers/_app';

export const trpc = createTRPCReact<AppRouter>();

2. Set up the Provider

Since tRPC runs on top of TanStack Query, you need to wrap your app in a Provider. Create the component src/components/Provider.tsx:

'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import React, { useState } from 'react';
import { trpc } from '@/utils/trpc';

export default function Provider({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [httpBatchLink({ url: 'http://localhost:3000/api/trpc' })],
    })
  );

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </trpc.Provider>
  );
}

Real-world Experience in a Component

This is where you see how great tRPC is. TypeScript will suggest exactly every property you wrote on the Server.

'use client';
import { trpc } from '@/utils/trpc';

export default function UserProfile() {
  const userQuery = trpc.getUser.useQuery({ id: '123' });

  if (userQuery.isLoading) return <div>Loading...</div>;

  return (
    <div>
      <h1>{userQuery.data?.name}</h1>
      <p>Email: {userQuery.data?.email}</p>
    </div>
  );
}

Try changing name to fullName on the Server. Immediately, the userQuery.data?.name line on the Client will show a red error. You don’t need to reload or run any scripts. Everything syncs instantly.

Performance Optimization and Debugging

Open the Network tab in Chrome, and you’ll see requests sent to /api/trpc/getUser. tRPC has a great feature called Batching. If a component calls 3 APIs at once, tRPC automatically combines them into a single request. This significantly reduces network latency.

Pro-tip: Don’t cram everything into a single _app.ts file. As the project grows, split it into userRouter, orderRouter, and use t.mergeRouters to combine them. The code will be much cleaner and easier to manage.

If you need to test complex Regex strings for Zod, check out toolcraft.app/en/tools/developer/regex-tester. It helps you quickly verify Regex accuracy without restarting the entire server.

Conclusion

tRPC is powerful, but it’s not a “silver bullet.” It works best in a Monorepo environment. If you’re building a Public API for third parties, REST or GraphQL are still safer choices. But for Fullstack TypeScript projects, tRPC is truly a productivity revolution. Happy coding with no more runtime errors!

Share: