Building a Next.js App with tRPC Client

When building a web application for a company as forward-thinking as Interstellar Solutions, type safety and performance are non-negotiable. We chose Next.js for its robust routing and server-side rendering capabilities, paired with tRPC to create a type-safe API layer that eliminates runtime errors in client-server communication.

To make this work, I set up a Next.js project, integrated tRPC, and configured the client to interact with our API routes. Below, I’ll walk you through implementing tRPC in a Next.js app, focusing on the client-side setup.

Setting Up tRPC in Next.js

tRPC allows us to define API procedures on the server and call them from the client with full TypeScript support. We started by setting up a tRPC router and integrating it with Next.js API routes.

Server-Side tRPC Router

First, we defined a tRPC router in server/api/router.ts:

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

const t = initTRPC.create();

export const appRouter = t.router({
  getUser: t.procedure
    .input(z.object({ id: z.string() }))
    .query(({ input }) => {
      return { id: input.id, name: `User ${input.id}`, email: `user${input.id}@example.com` };
    }),
  createUser: t.procedure
    .input(z.object({ name: z.string(), email: z.string().email() }))
    .mutation(({ input }) => {
      return { id: '123', ...input };
    }),
});

export type AppRouter = typeof appRouter;

This router defines two procedures: getUser (a query) and createUser (a mutation).

Next.js API Route for tRPC

We exposed the tRPC router via a Next.js API route in pages/api/trpc/[trpc].ts:

import { createNextApiHandler } from '@trpc/server/adapters/next';
import { appRouter } from '../../../server/api/router';

export default createNextApiHandler({
  router: appRouter,
  createContext: () => ({}),
});

This sets up the tRPC endpoint at /api/trpc/*.

Client-Side tRPC Integration

On the client, we used @trpc/react-query to integrate tRPC with React Query for data fetching. First, we configured the tRPC client in lib/trpc.ts:

import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/api/router';

export const trpc = createTRPCReact<AppRouter>();

Then, we wrapped our app with the tRPC provider in pages/_app.tsx:

import { trpc } from '../lib/trpc';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { useState } from 'react';
import type { AppProps } from 'next/app';

function MyApp({ Component, pageProps }: AppProps) {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        httpBatchLink({
          url: '/api/trpc',
        }),
      ],
    })
  );

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

export default MyApp;

Using tRPC in a Next.js Page

We used tRPC in a Next.js page to fetch and display user data. Here’s an example in pages/users/[id].tsx:

import { trpc } from '../../lib/trpc';
import { useRouter } from 'next/router';

export default function UserProfile() {
  const router = useRouter();
  const { id } = router.query;
  
  const { data: user, isLoading, error } = trpc.getUser.useQuery(
    { id: typeof id === 'string' ? id : '' },
    { enabled: typeof id === 'string' }
  );

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <h1>{user?.name}</h1>
      <p>{user?.email}</p>
    </div>
  );
}

This page uses the getUser query to fetch user data based on the dynamic route parameter id.

Next.js Routes

The Next.js application uses the following routes:

  1. / - Home page (not shown in the example, but typically defined in pages/index.tsx)
  2. /users/[id] - Dynamic route for user profile (e.g., /users/123)
  3. /api/trpc/[trpc] - tRPC API endpoint for all tRPC procedures

tRPC Procedure Routes

The tRPC router exposes the following procedure routes (accessible via the client):

  1. getUser - Query to fetch a user by ID (e.g., trpc.getUser.useQuery({ id: '123' }))
  2. createUser - Mutation to create a new user (e.g., trpc.createUser.useMutation())

Best Practices for Next.js and tRPC

  1. Type Safety: Leverage tRPC’s end-to-end type safety by sharing the AppRouter type between client and server.
  2. Dynamic Routes: Use Next.js dynamic routes ([id].tsx) to handle tRPC queries for specific resources.
  3. Error Handling: Implement robust error handling with React Query’s error state.
  4. Performance: Use React Query’s caching to minimize API calls and optimize client-side performance.

By combining Next.js’s routing with tRPC’s type-safe API, we built an application that’s both developer-friendly and scalable, ready to power Interstellar Solutions’ ambitious goals.