23 jul 2026

Introduction to tRPC

Learn the basics of tRPC and build a type-safe Posts App with Next.js step by step

Introduction to tRPC

1. Introduction

Type safety, no boilerplate, faster development.
That’s what tRPC brings to the table.

Building modern web applications often requires connecting the frontend and backend in a way that is efficient, scalable, and easy to maintain. Traditionally, developers rely on REST APIs or GraphQL to handle communication between the two. While these approaches are powerful, they usually come with extra overhead such as:

  • Writing API endpoint definitions
  • Duplicating types between client and server
  • Handling input validation manually
  • Maintaining API documentation

This can quickly become repetitive and error-prone, especially in projects where TypeScript is already being used end-to-end.

This is where tRPC comes in.

tRPC (TypeScript Remote Procedure Call) allows you to create type-safe APIs without the need for code generation or schema definitions. It gives you the power to call backend functions directly from your frontend while keeping full type safety, making your developer experience smoother and more productive.

In this article, we’re going to explore:

  • What tRPC is and its core benefits
  • The key concepts you need to know before using it
  • The architecture and data flow of a full-stack Next.js + tRPC application

Who is this for?

This guide is crafted for developers who want to bridge the gap between their client and server with as little friction as possible. Specifically, it’s a perfect fit if you fall into any of the following categories:

  • TypeScript Enthusiasts: If you’re already using TypeScript across your stack, tRPC is the "missing link" that allows your types to flow seamlessly from the database to the UI without manual synchronization.
  • Full-Stack Developers: If you're tired of the "context switching" that happens when you have to jump between defining a backend route and then manually fetching it on the frontend.
  • Next.js & React Users: While tRPC works with various frameworks, its integration with the React ecosystem is exceptionally polished, making it a go-to for modern web apps.
  • Efficiency Seekers: Developers who want to avoid the boilerplate of REST (defining endpoints, status codes, and JSON schemas) or the complexity of setting up a GraphQL server.

The tRPC Workflow at a Glance

To understand if this tool fits your mental model, consider how it simplifies the data-fetching lifecycle compared to traditional methods:

FeatureREST / GraphQLtRPC
Type SafetyRequires manual types or code-genInherent (Inferred from code)
BoilerplateHigh (Controllers, DTOs, Schemas)Low (Direct function exports)
RefactoringError-prone (Easy to break contracts)Safe (Rename a function, get instant IDE errors)

Note: If you are building a public-facing API that needs to be consumed by third parties (who might not use TypeScript), a traditional REST API is still the way to go. But for internal communication between your own frontend and backend, tRPC is a productivity powerhouse.

By the end, you’ll know how to:

  • Understand the core concepts of tRPC
  • Set up tRPC in a Next.js project
  • Structure routers and procedures
  • Connect your frontend with your backend seamlessly
  • Evaluate the strengths and limitations of tRPC for real-world projects

So, grab your coffee and let's dive in

Project Repository

You can find the complete source code for this walkthrough on GitHub:
tRPC Walkthrough Repository

Feel free to clone it, follow along, or use it as a reference while building your own tRPC application!


2. Index

Here’s a quick overview of what we’ll cover in this article:

  1. Introduction
    – Why tRPC is worth learning in modern web development.

  2. What’s tRPC?
    – A deep dive into what tRPC is, its features, where it shines, and where not to use it.

  3. Core Concepts Before Launching
    – Understanding the building blocks of tRPC:

    • Routers
    • Procedures
    • Queries vs Mutations
    • Context
    • Input Validation (with Zod)
    • Error Handling
    • Frontend Integration with React Query
  4. Let’s Build a Posts App!
    – A step-by-step walkthrough:

    • Setting up the project with Next.js and tRPC
    • Creating the postsRouter
    • Implementing CRUD operations (Create, Read, Update, Delete)
    • Connecting the frontend with hooks
    • Making everything type-safe and reactive
  5. Conclusion & Next Steps
    – Recap of what we’ve learned and suggestions for going further with tRPC.


3. What’s tRPC?

tRPC (TypeScript Remote Procedure Call) is a library that lets you expose server-side functions (procedures) and call them directly from the client with end-to-end TypeScript safety — no codegen, no manual client types. It’s ideal for same-repo TypeScript apps (Next.js, Remix, etc.) where you want fast DX and tight client-server integration.


Comparison: REST API vs tRPC

FeatureTraditional REST APItRPC
Type SafetyRequires external tools (e.g., OpenAPI, Swagger)End-to-end built-in
API EndpointsExplicitly defined (/api/users/:id)Procedural calls (api.user.get(id))
Code GenerationOften needed for client typesNone (infers types automatically)
Best ForPublic APIs consumed by various languagesInternal APIs in monorepos/same-repo

3.1 Core idea — simple & powerful

Instead of:

  • writing REST endpoints + client DTOs, or
  • defining GraphQL schemas + generating types,

with tRPC you write small TypeScript functions on the server, group them in routers, and the client calls them through typed hooks. The compiler ensures the client always matches the server signature.


3.2 Key features

  • End-to-end type safety — client inputs/outputs are inferred from server code.
  • No code generation — types come from the runtime definitions, not generated files.
  • Context & middleware — inject session, database, auth logic into every procedure.
  • Validation-first — works seamlessly with zod for runtime validation + static types.
  • React Query integration — built-in hooks (useQuery, useMutation) for caching, refetching, and optimistic updates.
  • Supports SSR & subscriptions — use in server-rendered pages and real-time apps (WebSocket or adapters).

3.3 Why developers love it (and why you might too)

  • Instant autocomplete for server procedures in your editor.
  • Fewer runtime bugs because mismatched shapes are caught at compile time.
  • Quicker iteration — add a procedure on the server and the client immediately knows its types.
  • Powerful integration with React Query features like caching, stale-while-revalidate, and optimistic updates.

3.4 Where tRPC shines

  • Internal tools, dashboards, SaaS admin panels.
  • Monorepos / single-repo full-stack apps where frontend + backend evolve together.
  • Teams using TypeScript across the stack who want to move fast and reduce friction.

3.5 When NOT to use tRPC

  • You need a public, language-agnostic API for third-party consumers (use REST/OpenAPI or GraphQL).
  • Your team is not using TypeScript or strongly prefers polyglot clients.
  • You require a strictly versioned, contract-first API strategy independent of your server codebase.

Quick mental model (one-liner)

tRPC = write server functions + call them from the client — with TypeScript preventing the surprises.


4. Core Concepts Before Launching

In this section we explain the core building blocks of tRPC and how they map to everyday mental models. Each subsection includes a short abstract example (TypeScript) to make the idea concrete without tying it to a specific database, framework, or repo.


4.1 Routers — “Departments” of your API

Concept:
A router groups related procedures (functions) — think of it as a department in a company. For example, postsRouter, authRouter, usersRouter.

Why:
Organization, encapsulation, and composability. Routers can be merged to create the app-wide appRouter.

Abstract example

// routers/posts.ts (abstract)
const postsRouter = t.router({
  list: t.procedure.query(() => { /* return posts list */ }),
  create: t.procedure.mutation(() => { /* create post */ }),
});

Metaphor: A router is like the “Sales” department: it handles everything related to sales. If you need sales features, you go to Sales (router).


4.2 Procedures — single API operations

Concept: A procedure is a single callable unit inside a router. It can be a query, mutation, or subscription.

Why: They are the actual operations the client calls. Each procedure declares input validation and returns typed outputs.

Abstract example

// single procedure
const getPost = t.procedure
  .input(z.string()) // expects a post id
  .query(({ input }) => { /* return post by id */ });

4.3 Queries vs Mutations — read vs write

Concept:

  • query = read-only (idempotent): fetch data.
  • mutation = side-effects (create/update/delete).

Why: Semantics matter for React Query caching, invalidation, and optimistic updates.

Abstract example

// queries
postsRouter = t.router({
  getAll: t.procedure.query(() => { /* fetch */ }),
  create: t.procedure.input(z.object({ title: z.string() })).mutation(({ input }) => { /* write */ }),
});

4.4 Context — shared request-scoped dependencies

Concept: context is an object created per-request that contains things like session, db client, request metadata.

Why: So procedures can easily access auth info, DB clients, feature flags, etc. Type the context to keep ctx typed throughout.

Abstract example

// context.ts (abstract)
export async function createContext({ req, res }) {
  const session = await getSessionFromReq(req);
  return { req, res, session, dbClient };
}
type Context = Awaited<ReturnType<typeof createContext>>;
const t = initTRPC.context<Context>().create();

Metaphor: Context is the briefcase you hand to each department head — it contains the tools they need for the current request.


4.5 Middleware — cross-cutting concerns

Concept: Middleware wraps procedures to run shared logic: auth checks, logging, rate limiting.

Why: Avoid repeating logic in each procedure; centralize policy enforcement.

Abstract example

// auth middleware (abstract)
const isAuthed = t.middleware(({ ctx, next }) => {
  if (!ctx.session?.user) throw new TRPCError({ code: 'UNAUTHORIZED' });
  return next({ ctx: { ...ctx, user: ctx.session.user } });
});
const protectedProcedure = t.procedure.use(isAuthed);

4.6 Input validation with Zod — safe contracts

Concept: Use zod schemas to validate inputs at runtime and infer TypeScript types automatically.

Why: Prevents invalid data from reaching business logic and ensures client / server types match.

Abstract example

const createPostSchema = z.object({
  title: z.string().min(1),
  content: z.string().optional(),
});

const createPost = t.procedure
  .input(createPostSchema)
  .mutation(async ({ input, ctx }) => {
    // input.title is typed and validated
  });

Metaphor: Zod is the security gate that verifies the shape of every package before it enters the building.


4.7 Error handling & TRPCError

Concept: Throw TRPCError from server procedures for consistent error codes and messages; the client receives structured errors.

Why: Standardized error flows make client-side handling predictable (401, 403, 400, 500, etc.)

Abstract example

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

if (!ctx.user) throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Please sign in' });

4.8 Transformers (e.g., SuperJSON) — safe serialization

Concept: Use a transformer to serialize/deserialize non-JSON-native types (Date, Map, BigInt).

Why: Avoid broken Date objects or BigInt when sending data between server and client.

Abstract example

const t = initTRPC.context<Context>().create({
  transformer: superjson,
});

4.9 Client integration — createTRPCReact + React Query

Concept: createTRPCReact<AppRouter>() creates strongly-typed hooks that integrate with React Query (useQuery, useMutation).

Why: You get caching, loading states, retries, optimistic updates — with full types.

Abstract example

// client utils (abstract)
export const trpc = createTRPCReact<AppRouter>();

// usage in component
const { data, isLoading } = trpc.posts.getAll.useQuery();
const create = trpc.posts.create.useMutation({ onSuccess: () => trpc.posts.getAll.invalidate() });


4.10 Subscriptions / Real-time updates

Concept: tRPC supports subscriptions for real-time updates (commonly via WebSocket or a separate realtime service). Alternatively, use third-party providers (Pusher, Supabase, Ably).

Why: For collaborative UIs, notifications, live dashboards.

Abstract example (conceptual)

// server pushes to a channel on mutation success (pseudo)
onPostUpdated => pubsub.publish('post-updates', payload);

// client subscribes
trpc.posts.onUpdate.useSubscription(undefined, {
  onData(update) { /* update cache/UI */ }
});

Note: Implementation details vary by adapter; this is an abstract pattern.


4.11 Testing & Type-safety

Concept: Because tRPC types are plain TypeScript, you can unit-test procedures directly (call them with a mocked ctx) and get full type coverage.

Why: Fast, type-safe server tests without much infra.

Abstract example

// test (pseudo)
const ctx = { session: { user: { id: 'u1' }}, dbMock };
const res = await postsRouter.create.resolve({ ctx, input: { title: 'x' }});
expect(res).toMatchObject({ title: 'x' });

Quick summary

  • Routers = groups of procedures (like departments).
  • Procedures = functions you call (query/mutation/subscription).
  • Context = request-scoped dependencies (session, db).
  • Middleware = reusable guards (auth, rate-limit).
  • Zod = validation + typed inputs.
  • React Query integration = powerful client-side caching + optimistic updates.
  • Transformers & error handling = reliable serialization + consistent errors.
  • Subscriptions = real-time patterns (adapter dependent).
  • Testing = call procedures with mocked ctx for fast unit tests.

4.12 Understanding tRPC Data Flow

Before moving to a real-world example, let's visualize how all the concepts above connect at runtime:

COuNl8F.md.png The diagram above illustrates the complete data flow in a tRPC application:

  1. Request Flow (Blue): User interactions trigger mutations in React components
  2. Response Flow (Red): Data flows back through tRPC hooks with type safety
  3. Cache Operations (Green): React Query manages caching and optimistic updates
  4. Authentication Layer: Protected procedures verify user sessions
  5. Service Layer: Business logic separated from API procedures
  6. Database Layer: Drizzle ORM handles all database operations

Pro tip: This architecture ensures type safety flows through every layer — from your database schema to your React components!


5. Full Working Example: WriteSpace Posts App

Now that you understand the core concepts, it's time to see them in action. Before diving into the repository, here's everything you need to bootstrap your own tRPC + Next.js project from scratch.

Quick Start: Scaffold Your Project

Run these commands to get a tRPC-ready Next.js app up and running:

# 1. Create a new Next.js app
npx create-next-app@latest my-trpc-app --typescript --app

# 2. Install tRPC and its dependencies
npm install @trpc/server @trpc/client @trpc/react-query @tanstack/react-query

# 3. Install Zod for input validation
npm install zod

# 4. Install SuperJSON for safe serialization
npm install superjson

# 5. Start the dev server
npm run dev

The four key files you'll need to create:

FilePurpose
src/server/api/trpc.tstRPC initialization, context, and middleware
src/server/api/root.tsMain app router combining all feature routers
src/app/api/trpc/[trpc]/route.tsNext.js App Router API handler
src/trpc/react.tsxClient-side React Query + tRPC provider

Once these are in place, you have a fully functional tRPC setup. From there, you can explore the power of tRPC by studying a complete, real-world implementation in the companion repository, where you can find WriteSpace — a complete posts platform with comments, likes, and real-time interactions using tRPC.

Instead of long code examples here, we have provided a fully documented repository. We suggest checking the corresponding README files in the repo for detailed explanations of:

  • Posts management with drafts and publishing
  • Nested comments system with infinite pagination
  • Likes with optimistic updates for instant feedback
  • Authentication-aware procedures using middleware
  • Type-safe end-to-end from database to UI

Check out the full tRPC Walkthrough Repository here!

The bottom line: With tRPC, you get the developer experience of a monolith with the flexibility of a distributed system — all while keeping your types in sync!

Ready to build your own tRPC app? Take this foundation and extend it with your own features. The patterns you've learned here will serve you well in any TypeScript full-stack project!

6. Resources to Go Further