System Design

Full Stack TypeScript: tRPC & Prisma End-to-End

Master full stack TypeScript development from database to frontend using tRPC and Prisma. Learn to build type-safe, high-performance applications with unparalleled developer experience.

Khader Vali calendar_today May 26, 2026 schedule 10 min read

Full Stack TypeScript: tRPC & Prisma End-to-End Development

As senior software engineers, we’re constantly on the hunt for tools and patterns that elevate our development experience, improve code quality, and boost productivity. In the vast and rapidly evolving landscape of web development, TypeScript has emerged as an undeniable champion, bringing static type checking to JavaScript and profoundly impacting how we build applications. But what if we could extend that type safety across the entire stack, from the database schema all the way to the frontend UI, with minimal boilerplate and maximum developer ergonomics?

Enter tRPC and Prisma. When combined with TypeScript, these two powerful libraries form an incredibly potent trifecta, enabling a full stack development experience that feels almost magical. This article will delve deep into building a modern, type-safe application from the ground up, leveraging TypeScript, tRPC, and Prisma. We’ll explore the ‘why’ behind this stack, walk through the ‘how’ with practical code examples, describe the architecture, and discuss real-world considerations.

The Quest for Full Stack Type Safety and Developer Experience

Before we dive into the specifics of tRPC and Prisma, let’s briefly reflect on the challenges that modern full stack development often presents:

  • API Layer Complexity: Building REST APIs involves defining endpoints, handling request/response bodies, writing documentation, and ensuring clients consume them correctly. GraphQL simplifies some aspects but introduces its own schema definition language and client-side setup. Both often require manual type synchronization between frontend and backend.
  • Data Access Layer Headaches: Interacting with databases traditionally involves writing SQL queries, mapping results to application objects, and managing connections. ORMs (Object-Relational Mappers) like TypeORM or Sequelize help, but still often require manual type inference or external tools to keep database schema types in sync with application code.
  • Type Discrepancy: The most significant pain point in a TypeScript-centric workflow is the impedance mismatch between backend API responses (often untyped or loosely typed JSON) and frontend expectations. This leads to runtime errors, a need for defensive programming, and a frustrating development loop where contract changes on the backend aren’t immediately reflected or caught on the frontend.

The ideal scenario is a seamless flow where changes in the database schema automatically propagate types to the backend data access layer, then to the API layer, and finally to the frontend, all without manual intervention. This is precisely what the tRPC + Prisma + TypeScript stack aims to achieve.

Why Full Stack TypeScript is a Game Changer

TypeScript isn’t just about catching errors; it’s about better code design, improved readability, and a superior developer experience. When applied end-to-end:

  • Early Error Detection: Catch bugs during development, not in production.
  • Enhanced Code Comprehension: Types act as self-documentation, making it easier to understand complex data structures and function signatures.
  • Superior Editor Support: Autocompletion, intelligent refactoring, and inline error checking dramatically speed up development.
  • Refactoring Confidence: Make large-scale changes with greater assurance that you haven’t broken existing contracts.

Understanding the Core Components

Prisma: The Next-Generation ORM

Prisma is an open-source ORM that helps app developers build faster and make fewer errors. It comprises three main parts:

  1. Prisma Schema: An intuitive, declarative data model that defines your database schema in a human-readable format.
  2. Prisma Client: A type-safe query builder generated from your Prisma Schema, tailored to your database. It allows you to interact with your database using plain TypeScript objects and methods.
  3. Prisma Migrate: A powerful migration system that automatically generates SQL migration files from changes in your Prisma Schema, making database schema evolution straightforward and trackable.

Prisma shines by generating a client that is fully type-safe, meaning all your database operations (queries, mutations, relations) are checked against your database schema at compile time. This eliminates common runtime errors related to incorrect field names or data types.

Full Stack TypeScript: tRPC & Prisma End-to-End
Generated Image

tRPC: Type-Safe APIs Without Schemas

tRPC stands for “TypeScript Remote Procedure Call.” It’s not a new protocol like REST or GraphQL; rather, it’s a framework that leverages TypeScript’s powerful inference capabilities to allow you to build fully type-safe APIs without defining any schemas or generating any code. You simply write TypeScript functions on your backend, and tRPC automatically infers their types and exposes them to your frontend.

The magic of tRPC lies in its ability to take your backend router’s types and make them directly available on your frontend client. This means:

  • No Code Generation: Unlike GraphQL, you don’t need to generate types from a schema file.
  • Seamless Type Inference: Your frontend client automatically knows the exact types of arguments, return values, and even potential errors for every API call.
  • Blazing Fast Developer Experience: Autocompletion and type checking for API calls become a standard part of your workflow, leading to fewer bugs and faster development.
  • Small Bundle Size: tRPC is lightweight and only includes the necessary client-side code.

tRPC operates on the principle that if your frontend and backend share the same TypeScript codebase (e.g., in a monorepo), you can achieve unparalleled type safety. It’s essentially direct function calls over the network, but with TypeScript ensuring everything lines up perfectly.

Architectural Overview: The tRPC & Prisma Stack

Let’s visualize the typical architecture when using tRPC and Prisma together:


+-------------------+       HTTP/WS       +-----------------------------+       TCP/IP       +-------------------+
|                   | <------------------> |                             | <----------------> |                   |
|  Frontend Client  |                      |       Backend Server        |                      |     Database      |
| (React/Next.js)   |                      | (Node.js/Next.js API Routes)|                      | (PostgreSQL/MySQL)|
|                   |                      |                             |                      |                   |
| - tRPC Client     |                      | - tRPC Server               |                      | - Prisma Schema   |
| - UI Components   |                      | - tRPC Routers              |                      | - Data            |
| - Data Fetching   |                      | - tRPC Context (Auth, etc.) |                      |                   |
|   (useQuery/Mutate)|                      | - Prisma Client             |                      |                   |
+-------------------+                      +-----------------------------+                      +-------------------+
        ^                                            ^
        | Type Inference                             | Type Inference
        | (from Backend)                             | (from Prisma Client)
        v                                            v
(Shared Type Definitions/Interfaces)

Here’s a breakdown of the data and type flow:

  1. Database Schema Definition: You start by defining your database models in schema.prisma. This is the source of truth for your data structure.
  2. Prisma Client Generation: From schema.prisma, Prisma generates a fully type-safe PrismaClient. This client understands all your models and their relations.
  3. Backend API Layer (tRPC):
    • Your backend application imports and uses the generated PrismaClient to interact with the database.
    • You define your API endpoints as regular TypeScript functions within tRPC routers. These functions receive arguments, interact with Prisma Client, and return data.
    • Crucially, tRPC infers the input types (using Zod for validation) and output types of these functions directly from their TypeScript signatures and the Prisma Client operations.
    • A tRPC context is typically created per request, providing access to the Prisma Client, authenticated user information, and other request-scoped data.
  4. Frontend Application (tRPC Client):
    • Your frontend application imports the types of your tRPC backend routers.
    • The tRPC client on the frontend uses these imported types to provide complete type safety and autocompletion for all your API calls.
    • When you call a backend procedure (e.g., trpc.user.getById.useQuery({ id: '123' })), TypeScript ensures that the input matches the backend’s expectations and that the returned data matches the backend’s inferred output type.

This entire process, from database schema to frontend component, is governed by TypeScript, eliminating the common disconnects between layers.

Setting Up Your Full Stack TypeScript Project

For a seamless developer experience, especially with tRPC, a monorepo setup is highly recommended. This allows the frontend to directly import types from the backend, facilitating tRPC’s powerful inference capabilities. Tools like Lerna, Turborepo, or simply a well-structured Yarn/NPM workspace can manage this. For this guide, we’ll assume a project structure where client and server share a common packages/ directory for utilities and types.

While you could start from scratch, a great accelerator is create-t3-app, which scaffolds a Next.js, tRPC, Prisma, NextAuth, and TailwindCSS project. However, to understand the underlying mechanics, we’ll walk through the setup step-by-step.

Initial Project Structure


my-fullstack-app/
├── packages/
│   ├── api/          # Backend server (e.g., Next.js API routes or dedicated Node.js server)
│   ├── web/          # Frontend client (e.g., Next.js app)
│   └── types/        # (Optional) Shared utility types, although tRPC often makes this less necessary
├── prisma/           # Prisma schema and migrations
│   └── schema.prisma
├── tsconfig.json     # Root TypeScript config
├── package.json      # Root package config for workspaces
└── ...

Let’s focus on the key parts: prisma/, packages/api/, and packages/web/.

1. Database Layer with Prisma

First, we need to set up Prisma and define our database schema. We’ll use PostgreSQL as an example, but Prisma supports various databases.

Install Prisma

Navigate to your project root and install Prisma CLI and client:


npm install prisma --save-dev
npm install @prisma/client
npx prisma init --datasource-provider postgresql # Or mysql, sqlite, etc.

This command creates a prisma/ directory with a schema.prisma file and sets up your .env file for the database connection string.

Define Your Prisma Schema

Open prisma/schema.prisma and define your models. Let’s create a simple User and Post model:


// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        String    @id @default(uuid())
  email     String    @unique
  name      String?
  posts     Post[]
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
}

model Post {
  id        String    @id @default(uuid())
  title     String
  content   String?
  published Boolean   @default(false)
  author    User      @relation(fields: [authorId], references: [id])
  authorId  String
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
}

This schema defines two models, User and Post, with a one-to-many relationship (a User can have many Posts). Notice the type declarations (String, Boolean, DateTime) and attributes like @id, @unique, @default, and @relation. These are declarative and easy to understand.

Run Migrations

Now, apply this schema to your database. Prisma Migrate generates the necessary SQL:


npx prisma migrate dev --name init # 'init' is just a name for your first migration

This command does three things:

  1. Creates a new migration file in prisma/migrations.
  2. Applies the migration to your database.
  3. Generates the Prisma Client based on your schema.

You can now find the generated Prisma Client in node_modules/@prisma/client. Its types are automatically available in your project.

2. API Layer with tRPC

Now, let’s set up our backend using Next.js API routes, which is a common and convenient way to integrate tRPC. We’ll put this in packages/api/.

Install tRPC and Dependencies


# In packages/api/
npm install @trpc/server @trpc/client @trpc/next @trpc/react-query @tanstack/react-query zod
npm install @types/node --save-dev # if not already present
  • @trpc/server: Core tRPC server logic.
  • @trpc/client: Core tRPC client logic (used by backend for type inference, and by frontend for actual calls).
  • @trpc/next: Adapters for Next.js API routes.
  • @trpc/react-query / @tanstack/react-query: Integration with React Query for powerful data fetching hooks.
  • zod: A TypeScript-first schema declaration and validation library, commonly used with tRPC for input validation.

Initialize tRPC Backend

We need a base router and a context factory. Create packages/api/src/trpc.ts:


// packages/api/src/trpc.ts
import { initTRPC } from '@trpc/server';
import superjson from 'superjson'; // For better serialization of dates, maps, etc.
import { ZodError } from 'zod';
import { PrismaClient } from '@prisma/client'; // Import Prisma Client

// Initialize Prisma Client
const prisma = new PrismaClient();

// Context factory: runs for each request, provides access to Prisma and auth info
export const createContext = async () => {
// In a real app, you'd parse headers for auth tokens here
// const user = await getUserFromToken(req.headers.authorization);
return {
prisma, // Make Prisma Client available in all tRPC procedures
// user, // Authenticated user info
};
};

/**
* Initialization of tRPC backend
* Should be done only once per backend!
*/
const t = initTRPC.context<typeof createContext>().create({
transformer: superjson, // Recommended for better serialization
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.code

Written by

Khader Vali

Senior Software Engineer specializing in cloud architecture, real-time systems, and enterprise-scale applications.

Share this article

Related Articles

Fault Tolerant Systems: Circuit Breakers, Retries, Bulkheads

May 27, 2026 · 16 min read

Understanding WebSocket Architecture at Enterprise Scale

Oct 24, 2024 · 2 min read

Event-Driven Architecture: When to Use It and When to Avoid It

Apr 28, 2026 · 2 min read