How to Use tRPC with Next.js 15 (App Router)

You've built a Next.js application and now you're looking to add a mobile app to your ecosystem. But as you explore your options, you're hit with a wave of uncertainty: How do you ensure type safety across your web and mobile clients? Will the current setup with Next.js App Router work well for mobile development? The thought of managing multiple API endpoints and maintaining type definitions across different platforms is enough to make any developer anxious.

Enter tRPC (TypeScript Remote Procedure Call) - a game-changing tool that brings end-to-end type safety to your full-stack TypeScript applications, making it an ideal choice for projects spanning web and mobile platforms.

Why Choose tRPC for Your Next.js Project?

Before diving into the implementation details, let's understand why tRPC might be the solution you're looking for:

1. End-to-End Type Safety Without the Hassle

Traditional REST APIs require you to maintain separate type definitions for your client and server, leading to potential inconsistencies and runtime errors. tRPC eliminates this problem by automatically inferring types across your entire application. No more manually syncing types or dealing with outdated API documentation!

2. Perfect for Mobile-First Development

When building for both web and mobile, tRPC shines by:

  • Providing a consistent API interface across platforms

  • Ensuring type safety for all API calls, regardless of the client

  • Reducing the likelihood of runtime errors that could affect user experience

3. Superior Developer Experience

Unlike Server Actions in Next.js, which can lead to tightly coupled code and limited revalidation options, tRPC offers:

  • Clear separation between client and server logic

  • Built-in support for complex data fetching patterns

  • Fine-grained control over API interactions

  • Excellent integration with React Query for caching and state management

4. Streamlined API Development

Say goodbye to:

  • Writing and maintaining separate REST or GraphQL schemas

  • Dealing with API documentation that's constantly out of date

  • Wrestling with type mismatches between frontend and backend

Getting Started with tRPC in Next.js 15

Let's set up tRPC in your Next.js project. First, you'll need to install the necessary dependencies:

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

For TypeScript projects, ensure you have strict mode enabled in your tsconfig.json:

{
  "compilerOptions": {
    "strict": true,
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "incremental": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve"
  }
}

Project Structure for Scalability

One of the most common concerns when setting up a tRPC project is organizing the codebase effectively, especially when planning for both web and mobile clients. Here's a recommended structure that has proven successful in production environments:

Monorepo Setup with Turborepo

For projects targeting both web and mobile platforms, a monorepo structure using Turborepo is highly recommended. Here's how to organize your project:

my-app/
├── apps/
│   ├── web/                 # Next.js application
│   │   ├── src/
│   │   │   ├── app/        # Next.js App Router pages
│   │   │   ├── trpc/      # tRPC client configuration
│   │   │   └── ...
│   │   └── package.json
│   └── mobile/             # React Native application
│       ├── src/
│       │   ├── api/       # tRPC client configuration
│       │   └── ...
│       └── package.json
├── packages/
│   ├── api/               # Shared tRPC router definitions
│   │   ├── src/
│   │   │   ├── routers/  # tRPC route handlers
│   │   │   ├── context.ts
│   │   │   └── root.ts   # Root router configuration
│   │   └── package.json
│   └── db/               # Database schema and utilities
│       ├── src/
│       │   ├── schema.ts
│       │   └── client.ts
│       └── package.json
└── package.json

Setting Up the tRPC Server

  1. First, create your tRPC instance in packages/api/src/trpc.ts:

import { initTRPC } from '@trpc/server';
import { Context } from './context';

const t = initTRPC.context<Context>().create();

export const router = t.router;
export const publicProcedure = t.procedure;
  1. Define your context in packages/api/src/context.ts:

import { inferAsyncReturnType } from '@trpc/server';
import * as trpcNext from '@trpc/server/adapters/next';

export async function createContext(opts: trpcNext.CreateNextContextOptions) {
  return {
    // Add your context here
    session: await getSession(opts.req, opts.res),
  };
}

export type Context = inferAsyncReturnType<typeof createContext>;
  1. Create your first router in packages/api/src/routers/example.ts:

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

export const exampleRouter = router({
  hello: publicProcedure
    .input(z.object({ text: z.string() }))
    .query(({ input }) => {
      return {
        greeting: `Hello ${input.text}`,
      };
    }),
  getAll: publicProcedure.query(({ ctx }) => {
    return ctx.prisma.example.findMany(); // If using Prisma
  }),
});
  1. Set up the root router in packages/api/src/root.ts:

import { router } from './trpc';
import { exampleRouter } from './routers/example';

export const appRouter = router({
  example: exampleRouter,
});

export type AppRouter = typeof appRouter;

Implementing tRPC in Your Next.js App

Now that we have our server-side setup complete, let's implement tRPC in your Next.js application:

Client-Side Setup

  1. Create a tRPC client utility in apps/web/src/trpc/client.ts:

import { createTRPCNext } from '@trpc/next';
import { httpBatchLink } from '@trpc/client';
import type { AppRouter } from '@my-app/api';

export const trpc = createTRPCNext<AppRouter>({
  config() {
    return {
      links: [
        httpBatchLink({
          url: '/api/trpc',
        }),
      ],
    };
  },
});
  1. Create the API route handler in apps/web/src/app/api/trpc/[trpc]/route.ts:

import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@my-app/api';
import { createContext } from '@my-app/api/context';

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

export { handler as GET, handler as POST };
  1. Set up the tRPC provider in your root layout (apps/web/src/app/layout.tsx):

import { headers } from 'next/headers';
import { TRPCReactProvider } from './providers';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html>
      <body>
        <TRPCReactProvider headers={headers()}>
          {children}
        </TRPCReactProvider>
      </body>
    </html>
  );
}

Using tRPC in Your Components

Now you can use tRPC in your components with full type safety:

'use client';

import { trpc } from '../trpc/client';

export default function MyComponent() {
  const hello = trpc.example.hello.useQuery({ text: 'World' });

  if (!hello.data) return <div>Loading...</div>;

  return (
    <div>
      <p>{hello.data.greeting}</p>
    </div>
  );
}

tRPC vs Server Actions vs API Routes

Let's compare these approaches to help you make an informed decision:

tRPC

✅ Pros:

  • Complete type safety across client and server

  • Excellent developer experience with automatic type inference

  • Perfect for complex applications with multiple clients

  • Built-in caching and state management through React Query

❌ Cons:

  • Initial setup complexity

  • Learning curve for developers new to RPC concepts

  • Requires TypeScript

Server Actions

✅ Pros:

  • Built into Next.js

  • Simple to implement for basic use cases

  • Works well with server components

❌ Cons:

API Routes

✅ Pros:

  • Familiar REST-like pattern

  • Easy to understand

  • Language-agnostic

❌ Cons:

  • No built-in type safety

  • Requires manual type maintenance

  • More boilerplate code

  • Need to handle serialization manually

Best Practices and Common Pitfalls

When implementing tRPC in your Next.js 15 project, keep these best practices in mind:

1. Organize Your Routers

Split your routers into logical domains:

// packages/api/src/routers/user.ts
export const userRouter = router({
  profile: publicProcedure
    .input(z.object({ userId: z.string() }))
    .query(async ({ input, ctx }) => {
      return ctx.prisma.user.findUnique({
        where: { id: input.userId },
      });
    }),
});

// packages/api/src/routers/post.ts
export const postRouter = router({
  list: publicProcedure
    .query(async ({ ctx }) => {
      return ctx.prisma.post.findMany();
    }),
});

2. Handle Errors Properly

Use Zod for input validation and create custom error handlers:

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

export const userRouter = router({
  updateProfile: publicProcedure
    .input(z.object({
      name: z.string().min(2),
      email: z.string().email(),
    }))
    .mutation(async ({ input, ctx }) => {
      try {
        return await ctx.prisma.user.update({
          where: { id: ctx.session?.user.id },
          data: input,
        });
      } catch (error) {
        throw new TRPCError({
          code: 'INTERNAL_SERVER_ERROR',
          message: 'Failed to update profile',
          cause: error,
        });
      }
    }),
});

3. Optimize for Mobile

When using tRPC with mobile clients, consider:

  • Implementing proper error handling for offline scenarios

  • Using React Query's caching capabilities effectively

  • Setting up appropriate timeout and retry logic

// apps/mobile/src/utils/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '@my-app/api';

export const trpc = createTRPCReact<AppRouter>({
  config() {
    return {
      links: [
        httpBatchLink({
          url: 'YOUR_API_URL',
          // Add custom headers for mobile
          headers() {
            return {
              'x-client-type': 'mobile',
            };
          },
        }),
      ],
      // Configure for mobile environment
      queryClientConfig: {
        defaultOptions: {
          queries: {
            retry: 2,
            cacheTime: 1000 * 60 * 60, // 1 hour
            staleTime: 1000 * 60 * 5, // 5 minutes
          },
        },
      },
    };
  },
});

Conclusion

tRPC with Next.js 15 provides a powerful solution for building type-safe APIs that work seamlessly across web and mobile platforms. While it may require some initial setup and learning, the benefits of end-to-end type safety and improved developer experience make it a compelling choice for modern full-stack applications.

Remember to:

  • Start with a well-organized monorepo structure

  • Implement proper error handling

  • Optimize for your target platforms

  • Take advantage of React Query's caching capabilities

For more information and examples, check out:

By following these guidelines and best practices, you'll be well-equipped to build robust, type-safe applications that scale across platforms while maintaining excellent developer experience and code quality.

Additional Resources

To help you get started with tRPC and Next.js 15, here are some valuable resources:

Learning Materials

Example Projects

Community Support

Remember, while tRPC might have a learning curve, its benefits in terms of type safety and developer experience make it a worthwhile investment for your Next.js projects, especially when building for both web and mobile platforms.

Raymond Yeh

Raymond Yeh

Published on 24 February 2025

Get engineers' time back from marketing!

Don't let managing a blog on your site get in the way of your core product.

Wisp empowers your marketing team to create and manage content on your website without consuming more engineering hours.

Get started in few lines of codes.

Choosing a CMS
Related Posts
Setting up tRPC with Next.js 14

Setting up tRPC with Next.js 14

Unlock the full potential of Next.js 14 by integrating it with tRPC! Follow this comprehensive guide to create type-safe, efficient applications—perfect for developers looking to optimize.

Read Full Story
TRPC & TanStack React Query - Now You Can Have Them Both!

TRPC & TanStack React Query - Now You Can Have Them Both!

tRPC + TanStack React Query integration guide: Learn to implement typesafe APIs with powerful data fetching, caching, and query invalidation in your React applications.

Read Full Story
Should I Just Use Next.js for Fullstack Development?

Should I Just Use Next.js for Fullstack Development?

Is Next.js the right fit for your fullstack project? Dive into its key features, challenges, and real developer experiences to make an informed choice.

Read Full Story
Loading...