The New T3 Stack in 2026 β Complete Updated Guide
- The T3 Stack's core value is the type chain: schema.prisma β Prisma Client β tRPC β React β one change propagates everywhere.
- tRPC v11 eliminates manual API type synchronization β define a procedure once, the client gets autocompletion and caching for free.
- shadcn/ui gives you component source code you own β no version-lock dependency, no abstraction fights.
- The T3 Stack is a full-stack TypeScript template combining Next.js, tRPC, Prisma, Tailwind CSS, and TypeScript for end-to-end type safety
- Next.js 15/16 App Router with Server Components eliminates API route boilerplate β data fetching happens directly in server-rendered components
- tRPC v11 provides end-to-end type inference from database schema through API layer to frontend components with zero code generation
- Prisma generates a type-safe client from your schema.prisma β every query is checked at compile time against your actual database schema
- shadcn/ui replaces traditional component libraries β you copy components into your project and own the code, no version-lock dependency
- The stack's power is the type chain: Prisma schema infers Prisma types, tRPC infers API types, React infers prop types β one change propagates everywhere
Production Incident
next build before prisma migrate deploy. The build used the old Prisma client (which still had legacy_status in its types). The migration then dropped the column. The deployed application had a client that referenced a column that no longer existed.prisma validate before build to catch schema drift. Added prisma generate as a postinstall script to ensure the client always matches the checked-in schema.Production Debug GuideCommon T3 Stack failures and how to diagnose them
useSuspenseQuery in a Client Component.The T3 Stack is not a framework β it's a curated set of tools that together provide end-to-end type safety from database to browser. Each tool solves a specific problem: Next.js for rendering and routing, tRPC for type-safe APIs, Prisma for type-safe database access, Tailwind for styling, and TypeScript as the connective tissue.
In 2026, T3 has split into two main patterns: (1) RSC-first: Server Components read directly from Prisma with Server Actions for writes (zero network hop), and (2) tRPC-everywhere: classic T3 with tRPC for both reads and writes. This guide covers pattern #2 because it is still the default create-t3-app setup, but weβll clearly call out where the RSC-first pattern wins.
The 2026 iteration has evolved significantly. Next.js 15/16βs App Router with Server Components changes where data fetching happens. tRPC v11 integrates natively with TanStack Query for caching and optimistic updates. Prismaβs edge runtime makes serverless deployments viable. shadcn/ui replaces bloated component libraries with copy-paste components you fully own.
This guide covers the architecture decisions, the type chain that makes the stack powerful, the production pitfalls that catch teams off guard, and the migration path from earlier versions.
The Type Chain: How the T3 Stack Propagates Types End-to-End
The T3 Stack's core value proposition is the type chain β a single change to your database schema propagates type safety through every layer of your application without manual synchronization.
It starts with Prisma's schema.prisma. When you define a model, Prisma generates a TypeScript client where every query is typed against your schema. The output of a Prisma query is a typed object that flows into your tRPC procedure. tRPC infers the procedure's output type from the return value. On the client, tRPC's useQuery hook infers the response type from the procedure definition. React components receive typed props from the tRPC response.
The chain: schema.prisma β Prisma Client types β tRPC procedure types β React component props. One change at the top propagates to the bottom. If you rename a column, TypeScript catches every reference that's now broken β in your API layer, your frontend components, and your form validations.
// io/thecodeforge/t3-stack/prisma/schema.prisma // This schema generates the type chain β every layer derives types from here generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id String @id @default(cuid()) email String @unique name String? role Role @default(MEMBER) posts Post[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("users") } model Post { id String @id @default(cuid()) title String content String published Boolean @default(false) author User @relation(fields: [authorId], references: [id]) authorId String createdAt DateTime @default(now()) @@index([authorId]) @@index([published, createdAt]) @@map("posts") } enum Role { ADMIN MEMBER VIEWER }
- Schema.prisma is the single source of truth for all data types in the application
- Prisma Client generates TypeScript types from the schema β every query is type-checked
- tRPC infers procedure input/output types from the Prisma Client return values
- React components infer prop types from tRPC's useQuery/useMutation hooks
- One schema change propagates type errors to every layer β TypeScript catches broken references at compile time
tRPC v11: Type-Safe APIs Without Code Generation
tRPC v11 eliminates the manual synchronization between backend API definitions and frontend type expectations. You define a procedure (query or mutation) on the server with a zod input schema and a typed resolver. The client imports the router type and gets full autocompletion, compile-time type checking, and zero code generation.
The v11 improvements over v10 include native TanStack Query v5 integration, better error handling with typed TRPCError responses, and improved middleware composition. The httpBatchLink now supports streaming β multiple procedures in a single HTTP request with progressive response delivery.
The key architectural decision is router composition. You define separate routers for each domain (users, posts, comments) and merge them into a root appRouter. The client sees a flat API surface: trpc.users.getById.useQuery() β the router hierarchy is invisible on the client side.
import { z } from 'zod'; import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc'; import { TRPCError } from '@trpc/server'; export const postRouter = createTRPCRouter({ // Public query β no auth required list: publicProcedure .input(z.object({ limit: z.number().min(1).max(100).default(20), cursor: z.string().nullish(), })) .query(async ({ ctx, input }) => { const posts = await ctx.db.post.findMany({ take: input.limit + 1, cursor: input.cursor ? { id: input.cursor } : undefined, orderBy: { createdAt: 'desc' }, include: { author: { select: { name: true, email: true } } }, }); let nextCursor: string | undefined; if (posts.length > input.limit) { nextCursor = posts.pop()!.id; } return { posts, nextCursor }; }), // Protected mutation β auth middleware applied create: protectedProcedure .input(z.object({ title: z.string().min(1).max(200), content: z.string().min(1), published: z.boolean().default(false), })) .mutation(async ({ ctx, input }) => { const post = await ctx.db.post.create({ data: { ...input, authorId: ctx.session.user.id, }, }); return post; }), // Protected query with authorization check getById: protectedProcedure .input(z.object({ id: z.string() })) .query(async ({ ctx, input }) => { const post = await ctx.db.post.findUnique({ where: { id: input.id }, include: { author: { select: { name: true } } }, }); if (!post) { throw new TRPCError({ code: 'NOT_FOUND', message: `Post ${input.id} not found`, }); } // Authorization: only author or admin can view unpublished posts if (!post.published && post.authorId !== ctx.session.user.id) { throw new TRPCError({ code: 'FORBIDDEN', message: 'You do not have access to this post', }); } return post; }), });
Prisma: Type-Safe Database Access in the Edge Runtime Era
Prisma generates a TypeScript client from your schema.prisma file. Every query β findMany, create, update, delete β is typed against your actual database schema. If you rename a column, TypeScript catches every query that references the old name at compile time.
The 2026 evolution is Prisma's edge runtime support. Traditional Prisma requires a Node.js binary for database connections β incompatible with edge runtimes (Cloudflare Workers, Vercel Edge Functions). Prisma's driver adapters (for Neon, PlanetScale, Turso) enable edge-compatible database access by using platform-native drivers instead of Prisma's binary engine.
The architectural decision is connection pooling. In serverless/edge environments, each function invocation opens a new database connection. Without pooling, you exhaust the database's connection limit at ~100 concurrent requests. Prisma Accelerate or a database-native pooler (PgBouncer, Neon's built-in pooler) is mandatory for production serverless deployments.
import { PrismaClient } from '@prisma/client'; import { withAccelerate } from '@prisma/extension-accelerate'; // Singleton pattern: prevent multiple PrismaClient instances in development // Hot reloading creates new instances on every file change without this const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined; }; // In production, use Prisma Accelerate for connection pooling and edge caching // In development, use the standard client export const db = globalForPrisma.prisma ?? new PrismaClient({ log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], }).$extends(withAccelerate()); if (process.env.NODE_ENV !== 'production') { globalForPrisma.prisma = db; } // Usage in Server Components and tRPC procedures: // const posts = await db.post.findMany({ // where: { published: true }, // orderBy: { createdAt: 'desc' }, // take: 20, // });
shadcn/ui: Owning Your Component Code Instead of Depending on It
shadcn/ui is not a component library β it's a collection of copy-paste components built on Radix UI primitives and styled with Tailwind CSS. You run npx shadcn-ui@latest add button and the component source code is copied into your project. You own it. You modify it. There's no version-locked dependency to update.
This solves a real production problem: traditional component libraries (MUI, Ant Design, Chakra) ship as npm packages. When you need to customize a component beyond the library's API surface, you fight the abstraction. shadcn/ui gives you the source code β customization is just editing a file in your project.
The architecture is built on three layers: Radix UI provides accessible primitives (keyboard navigation, ARIA attributes, focus management), Tailwind CSS provides the styling system (utility classes, CSS variables for theming), and shadcn/ui provides the composition layer that combines them into production-ready components.
// Custom DataTable component built on shadcn/ui primitives // This is YOUR code β modify it to match your design system 'use client'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { ChevronLeft, ChevronRight, Search } from 'lucide-react'; import { useState } from 'react'; interface Column<T> { key: keyof T; header: string; render?: (value: T[keyof T], row: T) => React.ReactNode; } interface DataTableProps<T> { data: T[]; columns: Column<T>[]; pageSize?: number; searchable?: boolean; } export function DataTable<T extends { id: string }>({ data, columns, pageSize = 10, searchable = true, }: DataTableProps<T>) { const [page, setPage] = useState(0); const [search, setSearch] = useState(''); const filtered = search ? data.filter((row) => columns.some((col) => String(row[col.key]).toLowerCase().includes(search.toLowerCase()) ) ) : data; const paginated = filtered.slice(page * pageSize, (page + 1) * pageSize); const totalPages = Math.ceil(filtered.length / pageSize); return ( <div className="space-y-4"> {searchable && ( <div className="relative"> <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <Input placeholder="Search..." value={search} onChange={(e) => { setSearch(e.target.value); setPage(0); }} className="pl-9" /> </div> )} <Table> <TableHeader> <TableRow> {columns.map((col) => ( <TableHead key={String(col.key)}>{col.header}</TableHead> ))} </TableRow> </TableHeader> <TableBody> {paginated.map((row) => ( <TableRow key={row.id}> {columns.map((col) => ( <TableCell key={String(col.key)}> {col.render ? col.render(row[col.key], row) : String(row[col.key])} </TableCell> ))} </TableRow> ))} </TableBody> </Table> <div className="flex items-center justify-between"> <p className="text-sm text-muted-foreground"> Showing {page * pageSize + 1}-{Math.min((page + 1) * pageSize, filtered.length)} of {filtered.length} </p> <div className="flex gap-2"> <Button variant="outline" size="sm" disabled={page === 0} onClick={() => setPage(page - 1)}> <ChevronLeft className="h-4 w-4" /> </Button> <Button variant="outline" size="sm" disabled={page >= totalPages - 1} onClick={() => setPage(page + 1)}> <ChevronRight className="h-4 w-4" /> </Button> </div> </div> </div> ); }
Project Structure: Organizing the T3 Stack for Scale
The T3 Stack's project structure determines how maintainable the codebase is at scale. A flat structure works for prototypes but collapses at 50+ procedures and 100+ components. The recommended structure separates concerns by domain, not by technology layer.
The key organizational principle is domain-driven: each feature (users, posts, comments) gets its own directory containing its Prisma model, tRPC router, Server Components, Client Components, and shared types. This keeps related code together β when you modify the posts feature, everything you need is in one directory.
The alternative β organizing by technology layer (all routers in one directory, all components in another) β forces you to jump between 5 directories to understand a single feature. At scale, this cognitive overhead slows development to a crawl.
// Recommended T3 Stack project structure for production scale // Domain-driven organization β each feature is self-contained io/thecodeforge/t3-stack/ βββ src/ β βββ app/ # Next.js App Router β β βββ layout.tsx # Root layout (Server Component) β β βββ page.tsx # Landing page β β βββ dashboard/ # Dashboard feature β β β βββ layout.tsx # Dashboard layout with sidebar β β β βββ page.tsx # Dashboard overview (Server Component) β β β βββ posts/ # Posts sub-feature β β β β βββ page.tsx # Posts list (Server Component) β β β β βββ [id]/page.tsx # Post detail (Server Component) β β β β βββ new/page.tsx # Create post (Client Component form) β β β βββ settings/ # Settings sub-feature β β β βββ page.tsx # Settings page β β βββ api/ β β βββ trpc/[trpc]/route.ts # tRPC API handler β β β βββ server/ # Server-side code β β βββ trpc/ β β β βββ trpc.ts # tRPC initialization + context β β β βββ routers/ β β β β βββ _app.ts # Root router (merges all routers) β β β β βββ post.ts # Post procedures β β β β βββ user.ts # User procedures β β β βββ middleware.ts # Auth, rate limiting middleware β β βββ auth.ts # Auth.js v5 configuration β β β βββ components/ # Shared UI components β β βββ ui/ # shadcn/ui components (owned code) β β β βββ button.tsx β β β βββ table.tsx β β β βββ input.tsx β β βββ shared/ # App-specific shared components β β βββ header.tsx β β βββ sidebar.tsx β β β βββ lib/ # Shared utilities β β βββ db.ts # Prisma client singleton β β βββ utils.ts # cn() helper, formatters β β βββ validations/ # Shared zod schemas β β βββ post.ts β β βββ user.ts β β β βββ prisma/ β β βββ schema.prisma # Database schema (source of truth) β β βββ migrations/ # Migration history β β β βββ tailwind.config.ts # Tailwind configuration β βββ components.json # shadcn/ui configuration β βββ next.config.js # Next.js configuration
| Component | Problem It Solves | What It Replaces | 2026 Evolution |
|---|---|---|---|
| Next.js 15/16 | Rendering, routing, data fetching | Express + React SPA, manual SSR setup | App Router, Server Components, Server Actions, PPR |
| tRPC v11 | Type-safe API layer without code generation | REST/GraphQL with manual type definitions | Native TanStack Query v5, streaming, better error types |
| Prisma | Type-safe database queries from schema | Raw SQL, Sequelize, TypeORM | Edge runtime driver adapters, Prisma Accelerate |
| Tailwind CSS | Utility-first styling with design tokens | CSS modules, styled-components, Sass | Oxide engine (10x faster builds), container queries |
| shadcn/ui | Owned component code with accessible primitives | MUI, Ant Design, Chakra (dependency-locked) | New York style, improved theming, date picker |
| TypeScript | End-to-end type safety across the stack | JavaScript (no type checking) | Satisfies operator, const type parameters, isolated declarations |
π― Key Takeaways
- The T3 Stack's core value is the type chain: schema.prisma β Prisma Client β tRPC β React β one change propagates everywhere.
- tRPC v11 eliminates manual API type synchronization β define a procedure once, the client gets autocompletion and caching for free.
- shadcn/ui gives you component source code you own β no version-lock dependency, no abstraction fights.
- Connection pooling is mandatory for serverless Prisma β without it, you exhaust database connections at ~100 concurrent users.
- Organize by domain, not by layer β one directory per feature keeps related code together at scale.
β Common Mistakes to Avoid
Interview Questions on This Topic
- QExplain the type chain in the T3 Stack. How does a schema change propagate type safety through the entire application?Mid-levelReveal
- QHow does tRPC differ from REST and GraphQL? When would you choose one over the others?Mid-levelReveal
- QWhat is the difference between shadcn/ui and a traditional component library like MUI? Why would you choose one over the other?JuniorReveal
- QHow does Prisma's edge runtime support work, and why is connection pooling mandatory in serverless?SeniorReveal
- QWalk me through the recommended CI/CD pipeline for a T3 Stack project. What order should build steps run in, and why?SeniorReveal
Frequently Asked Questions
Can I use the T3 Stack without tRPC?
Yes. The T3 Stack is modular β you can replace tRPC with REST API routes, GraphQL, or Server Actions. The type chain still works through Prisma, but you lose end-to-end inference on the API layer. You'd need to manually define request/response types or use OpenAPI codegen. tRPC is the biggest type-safety multiplier in the stack β removing it is possible but leaves a gap in the chain.
How does shadcn/ui handle component updates?
shadcn/ui components are copied into your project β they don't update automatically like npm packages. Run npx shadcn-ui@latest diff to see what changed upstream. Then manually merge the changes into your customized components. This is intentional: you own the code, so you control when and how updates are applied. No breaking changes surprise you in a minor version bump.
What database should I use with the T3 Stack in 2026?
PostgreSQL is the default and recommended choice β Prisma has the deepest support, and edge-compatible options (Neon, Supabase) are mature. MySQL is fully supported. SQLite works for local development and small apps. For edge deployments, use Neon (Postgres) or Turso (SQLite) with Prisma's driver adapters. Avoid databases without Prisma support unless you're willing to write raw SQL.
Can I use the T3 Stack with a monorepo?
Yes β the T3 Stack works well in Turborepo or Nx monorepos. The recommended pattern: put the Prisma schema and generated client in a shared package, import it into the Next.js app and any other services. Share zod validation schemas the same way. This prevents schema drift between services that share the same database.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.