
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
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;
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>;
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
}),
});
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
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',
}),
],
};
},
});
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 };
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:
Limited type safety
Can lead to tightly coupled code
Less suitable for complex client-server interactions
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:
Create T3 App - A great starting point for new projects
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.