End-to-End Type Safety Using tRPC and TanStack Query

Achieve complete end-to-end type safety between your backend and React frontend using tRPC and TanStack Query without writing manual API contracts or schemas.

In traditional full-stack web applications, maintaining consistency between API endpoints and frontend data consumption requires constant synchronization. Writing manual TypeScript interfaces, OpenAPI specifications, or GraphQL schemas often leads to drift, runtime validation bugs, and mismatched payload structures when backend controllers change.

tRPC eliminates this friction entirely. By leveraging TypeScript's advanced type inference systems, tRPC allows you to share types directly between your server and client without code generation or manual schema duplication.

This comprehensive guide explores how to build end-to-end type-safe APIs using tRPC and TanStack Query, covering backend router definitions, client setup, query execution, and mutation handling in React.

The Power of Full-Stack Type Inference

To appreciate the synergy between tRPC and TanStack Query, it helps to examine the architectural benefits they bring to modern React applications:

• Zero Schema Duplication: Your backend router definition acts as the single source of truth for types. If you change a database field or rename a return property on the server, TypeScript instantly flags errors in every React component consuming that endpoint.

• Native TanStack Query Integration: `@trpc/react-query` bridges tRPC routers directly with TanStack Query hooks, giving you automatic caching, background refetching, pagination, and optimistic updates with full type safety out of the box.

• Lightning-Fast Developer Experience: Autocomplete works seamlessly across query inputs, parameters, and response structures without needing to switch context or inspect network tabs.

Defining App Routers and Procedures

To start using tRPC, install the core packages and initialize your tRPC server context and router instances.

BASH
Installing tRPC and TanStack Query packages.
npm install @trpc/server @trpc/client @trpc/react-query @tanstack/react-query zod
TypeScript
Defining a type-safe tRPC router with input validation using Zod.
import { initTRPC } from '@trpc/server';
import { z } from 'zod';

const t = initTRPC.create();

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

interface User {
  id: string;
  name: string;
  email: string;
}

const users: User[] = [
  { id: '1', name: 'Alice Smith', email: 'alice@example.com' },
  { id: '2', name: 'Bob Johnson', email: 'bob@example.com' },
];

export const appRouter = router({
  getUserById: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(({ input }) => {
      const user = users.find((u) => u.id === input.id);
      return user || null;
    }),
  createUser: publicProcedure
    .input(z.object({ name: z.string(), email: z.string().email() }))
    .mutation(({ input }) => {
      const newUser: User = { id: String(users.length + 1), ...input };
      users.push(newUser);
      return newUser;
    }),
});

export type AppRouter = typeof appRouter;

Fetching and Mutating Data with Full Type Safety

On the React frontend, you can use the generated `AppRouter` type to initialize your tRPC React hooks, enabling fully type-safe queries and mutations.

TSX
Consuming tRPC queries and mutations inside a React component.
'use client';

import { trpc } from '@/utils/trpc';
import { useState } from 'react';

export default function UserProfile({ userId }: { userId: string }) {
  const utils = trpc.useUtils();
  const { data: user, isLoading, error } = trpc.getUserById.useQuery({ id: userId });

  const [newName, setNewName] = useState('');

  const createUserMutation = trpc.createUser.useMutation({
    onSuccess: () => {
      utils.getUserById.invalidate();
    },
  });

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

  return (
    <div className="p-6 bg-white rounded-xl shadow max-w-lg mx-auto">
      <h2 className="text-2xl font-bold mb-2">User Profile</h2>
      {user ? (
        <div className="space-y-1 mb-6">
          <p className="text-gray-800 font-medium">Name: {user.name}</p>
          <p className="text-gray-600">Email: {user.email}</p>
        </div>
      ) : (
        <p className="text-gray-500 mb-6">User not found.</p>
      )}

      <div className="border-t pt-4">
        <h3 className="text-lg font-semibold mb-2">Add New User</h3>
        <form
          onSubmit={(e) => {
            e.preventDefault();
            createUserMutation.mutate({ name: newName, email: `${newName.toLowerCase()}@example.com` });
          }}
          className="flex gap-2"
        >
          <input
            type="text"
            placeholder="Full Name"
            value={newName}
            onChange={(e) => setNewName(e.target.value)}
            className="px-3 py-2 border rounded-lg flex-1"
          />
          <button type="submit" className="px-4 py-2 bg-indigo-600 text-white rounded-lg">
            Create
          </button>
        </form>
      </div>
    </div>
  );
}

Summary

Combining tRPC with TanStack Query provides the ultimate end-to-end type safety experience for full-stack React and Next.js applications.

By eliminating manual API contracts, schema duplication, and runtime payload mismatches, developers can ship features faster while maintaining absolute confidence in their type definitions across the entire stack.