20+ years in enterprise software — production Java systems serving millions of transactions,
large-scale batch automation in banking & fintech. All examples on this site are drawn from real systems.
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
✦ Definition~90s read
What is T3 Stack Gotcha?
The T3 Stack is a curated, opinionated TypeScript-first web development stack created by Theo Browne (t3.gg) that solves the fundamental impedance mismatch between frontend and backend types. It combines Next.js (App Router), tRPC, Prisma, NextAuth.js, and Tailwind CSS into a single coherent architecture where types flow end-to-end without code generation or manual API contracts.
★
Imagine building a house where the architect's blueprint automatically updates the electrician's wiring plan, the plumber's pipe layout, and the carpenter's measurements every time you move a wall.
The stack's core insight is that if your entire application—database schema, API layer, authentication, and UI—is written in TypeScript, you can eliminate entire categories of bugs by having the compiler verify that your frontend code correctly uses the same types your backend defines. This isn't just a starter template; it's a deliberate constraint system that forces type safety at every layer, from your Prisma schema to your React components.
The T3 Stack exists because traditional full-stack TypeScript setups suffer from type fragmentation: your Prisma types don't automatically propagate to your API responses, your API types don't flow into your React components, and you end up writing duplicate type definitions or relying on brittle code generation. By using tRPC as the glue layer, the T3 Stack makes your backend procedures directly callable from the frontend with full type inference—no REST endpoints, no GraphQL resolvers, no OpenAPI specs.
Prisma provides the database layer with generated types that tRPC can consume directly, while NextAuth.js handles authentication with types that integrate into both Prisma and tRPC. The result is a development experience where changing a column in your database schema automatically updates the types available in your React components, and the TypeScript compiler catches mismatches before they reach production.
Where the T3 Stack shines is in full-stack TypeScript applications where type safety and developer velocity are paramount—think SaaS products, internal tools, or any project where a single team owns both frontend and backend. It's less suitable for projects that need to expose public REST APIs to third parties, require polyglot backends (e.g., Python ML services), or have existing non-TypeScript infrastructure.
The stack also assumes you're comfortable with its specific tool choices: if you prefer Drizzle over Prisma, or want to use React Query directly instead of tRPC's built-in caching, you're working against the grain. Real-world adoption includes companies like Cal.com, Dub.co, and countless startups that value the stack's ability to reduce boilerplate and catch type errors at compile time rather than runtime.
The tradeoff is that you're buying into a tightly coupled ecosystem where upgrading one piece (like tRPC v10 to v11) can cascade through your entire codebase—which is exactly the kind of gotcha this article addresses.
Plain-English First
Imagine building a house where the architect's blueprint automatically updates the electrician's wiring plan, the plumber's pipe layout, and the carpenter's measurements every time you move a wall. That's the T3 Stack — change your database schema, and the type system automatically updates your API contracts, your frontend components, and your form validations. No manual synchronization, no type drift, no 'I forgot to update the frontend' bugs.
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.
Why T3 Stack Is More Than a Starter Template
The T3 Stack is a curated, opinionated full-stack TypeScript framework combining tRPC, Prisma, Next.js, and Tailwind CSS. Its core mechanic is end-to-end type safety: you define a Prisma schema, tRPC automatically infers types from your database queries, and the client consumes those types without any code generation step. This eliminates the traditional REST/GraphQL boundary where type definitions drift from implementation.
In practice, the T3 Stack enforces a strict server-first architecture. All data fetching happens in Next.js server components or tRPC procedures, never on the client. This means your Prisma client runs exclusively on the server, avoiding the common mistake of leaking database credentials to the browser. The stack also mandates Zod for input validation at the API layer, giving you runtime type checking that matches your compile-time types.
Use the T3 Stack when you're building a data-intensive web application where type safety across the entire stack is non-negotiable. It shines in teams of 3-10 developers who want to move fast without sacrificing correctness. The real win is catching schema mismatches at compile time rather than in production — a Prisma migration that breaks your API surfaces immediately as a type error, not a runtime crash.
Stale Prisma Client After Migration
Running prisma migrate dev does not automatically regenerate the Prisma client — you must run prisma generate separately or the old client will silently use the wrong schema.
Production Insight
A team ran a migration that added a required column, deployed without running prisma generate, and the old client inserted rows with a null value for the new column, causing a cascade of foreign key violations.
The symptom: no TypeScript error, no build failure — just silent data corruption in production that only surfaced hours later when a downstream service tried to join on the missing column.
Rule of thumb: always run prisma generate immediately after any migration, and add a CI check that fails if the generated client is older than the latest migration file.
Key Takeaway
Prisma client is not automatically regenerated after migrations — you must run prisma generate explicitly.
A stale client can silently write invalid data to your database with zero compile-time warnings.
Always add a CI step that compares the migration timestamp with the generated client timestamp.
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")
}
enumRole {
ADMINMEMBERVIEWER
}
Output
Prisma Client generated: User, Post, Role types available throughout the stack
Type Chain Mental Model
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
Production Insight
The type chain catches rename and deletion errors at compile time — no runtime surprises.
If the chain breaks (stale Prisma client), TypeScript compiles but runtime queries fail.
Rule: always regenerate Prisma client after schema changes — stale types are worse than no types.
Key Takeaway
The type chain is the T3 Stack's core value — one schema change propagates type safety to every layer.
Prisma schema → Prisma Client → tRPC procedures → React props — each layer infers from the previous.
Punchline: if the Prisma client is stale, the type chain is broken — TypeScript compiles but runtime queries fail.
Type Chain Design Decisions
IfAdding a new field to a model
→
UseAdd to schema.prisma, run prisma generate, types propagate to tRPC and React automatically
UseRemove from schema.prisma + migration, regenerate client — TypeScript catches every usage that needs cleanup
IfNeed a computed/derived type not in the database
→
UseDefine it in a shared types module and import into both tRPC and React — don't add phantom columns to Prisma
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.
tRPC middleware executes in the order it's chained. If you add auth middleware before input validation, an unauthenticated request with invalid input will fail with an auth error instead of a validation error. Chain validation middleware first, then auth — so the user gets the most relevant error message.
Production Insight
tRPC eliminates the manual sync between backend routes and frontend type expectations.
Router composition keeps the codebase modular — each domain has its own router file.
Rule: define separate routers per domain (users, posts, comments) and merge into a root appRouter.
Key Takeaway
tRPC v11 gives end-to-end type inference from database to browser with zero code generation.
Router composition keeps the codebase modular — one router per domain, merged into appRouter.
Punchline: define a procedure once on the server — the client gets autocompletion, type checking, and TanStack Query caching for free.
UseUse query — TanStack Query caches and deduplicates automatically
IfProcedure modifies data (create, update, delete)
→
UseUse mutation — not cached, triggers query invalidation
IfNeed real-time updates (chat, notifications)
→
UseUse subscription with wsLink — WebSocket-based push from server to client
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.
io/thecodeforge/t3-stack/lib/db.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 thisconst globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
// In production, use Prisma Accelerate for connection pooling and edge caching// In development, use the standard clientexportconst db = globalForPrisma.prisma ?? newPrismaClient({
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,// });
Output
Prisma Client initialized with Accelerate extension — connection pooling and edge caching enabled
Pro Tip: Always Add @@index for Foreign Keys and Filtered Columns
Prisma doesn't automatically create indexes on foreign keys in all databases. Add @@index([fieldName]) explicitly for every foreign key and every column you filter or sort by. A missing index on a frequently queried column causes full table scans — query latency goes from 2ms to 500ms at 100K rows.
Production Insight
Prisma edge runtime requires driver adapters — the traditional binary engine doesn't work on Cloudflare Workers.
Connection pooling is mandatory in serverless — without it, you exhaust DB connections at ~100 concurrent requests.
Rule: use PgBouncer, Prisma Accelerate, or Neon built-in pooling for production serverless deployments.
Key Takeaway
Prisma generates a type-safe client from schema.prisma — every query is checked at compile time.
Edge runtime requires driver adapters — the traditional binary engine doesn't work outside Node.js.
Punchline: connection pooling is mandatory in serverless — without it, you hit the database connection limit at ~100 concurrent requests.
Prisma Deployment Decisions
IfDeploying to Node.js server (VPS, container)
→
UseStandard Prisma Client — binary engine, direct connection
IfDeploying to edge runtime (Cloudflare Workers, Vercel Edge)
→
UseUse driver adapter (Neon, PlanetScale, Turso) — no binary engine available
IfDeploying to serverless (Vercel Functions, AWS Lambda)
→
UseUse Prisma Accelerate for connection pooling — prevents connection exhaustion
IfDatabase has 100K+ rows and queries are slow
→
UseAdd @@index on filtered/sorted columns, run EXPLAIN ANALYZE on generated SQL
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.
DataTable component built on shadcn/ui primitives — fully owned, customizable, type-safe
Why shadcn/ui Wins Over Traditional Component Libraries
Traditional libraries (MUI, Ant Design) give you a component as a black box. When the design team wants a button that doesn't match the library's API, you fight overrides, CSS specificity wars, and theme provider hacks. shadcn/ui gives you the source code — the button IS your button. Customize it like any other file in your project.
Production Insight
shadcn/ui components are your code — no version-lock dependency to update or break.
Radix UI primitives handle accessibility (ARIA, keyboard nav) — you never need to rebuild a11y from scratch.
Rule: run npx shadcn-ui@latest diff before updating components — see what changed upstream before merging.
Key Takeaway
shadcn/ui is not a library — it's copy-paste component code you own and modify.
Punchline: you own the source code — customize it like any other file. No version-lock dependency, no abstraction fights.
Component Library Decisions
IfNeed full customization control over every component
→
UseUse shadcn/ui — you own the source code, modify anything
IfNeed a large pre-built component set with strict design system
→
UseConsider MUI or Ant Design — more opinionated, faster initial setup
IfBuilding a design system from scratch
→
UseUse shadcn/ui as a foundation — Radix primitives + Tailwind tokens = custom design system
IfTeam doesn't want to maintain component code
→
UseUse a traditional library — shadcn/ui requires you to own and maintain the components
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.
Domain-driven structure: related code together — one directory per feature, not one per technology layer.
Pro Tip: Share Zod Schemas Between Server and Client
Define zod validation schemas in lib/validations/ and import them into both tRPC procedures and React form components. This prevents validation drift — one schema validates on the server and the client, with identical rules.
Production Insight
Domain-driven structure keeps related code together — one directory per feature.
Shared zod schemas in lib/validations/ prevent validation drift between server and client.
Punchline: organize by domain, not by layer — cognitive overhead kills velocity at 50+ procedures.
Key Takeaway
Domain-driven structure keeps related code together — one directory per feature, not one per technology.
Shared zod schemas in lib/validations/ prevent validation drift between server and client.
Punchline: organize by domain, not by layer — cognitive overhead kills velocity at 50+ procedures.
Project Structure Decisions
IfSmall project with fewer than 10 procedures and 20 components
→
UseFlat structure is fine — domain-driven adds overhead for small codebases
IfMedium project with 10-50 procedures across multiple features
→
UseUse domain-driven structure — one directory per feature with colocated router, components, and types
IfLarge project with 50+ procedures and multiple teams
→
UseUse domain-driven with package boundaries — each feature could become a separate npm package
IfShared types needed across multiple features
→
UsePlace in lib/validations/ or lib/types/ — single source of truth, imported by all features
Auth That Doesn't Leak: NextAuth.js in Production
You don't get to skip authentication just because you're building a portfolio. The T3 stack ships NextAuth.js for a reason — it plugs into your Prisma schema, your tRPC context, and your type chain without forcing you to write glue code.
The trap is thinking auth is just a login button. It's not. It's session invalidation, role guards, and audit trails. NextAuth.js gives you adapters for Prisma out of the box. That means your User model, Session model, and Account model are all typed end-to-end. When you call getServerSession in a tRPC middleware, the user object is fully typed. No type assertions. No runtime surprises.
The real win: you define your auth logic once, and every route, every query, every mutation inherits it. You don't scatter if (!session) in every resolver. You wrap your procedures with protectedProcedure and move on. That's the difference between a demo and a deploy.
No output — this is a middleware pattern. The tRPC resolver returns the created guestbook entry object.
Production Trap:
Never trust session.user directly. Always validate against your database in the middleware. Sessions can be stale or tampered with if you're not using JWT on the edge.
Key Takeaway
Wrap auth into a tRPC middleware once, reuse protectedProcedure everywhere. Don't repeat session checks.
Edge Runtime: Prisma Without the Connection Pool Panic
Edge functions are stateless and short-lived. That kills traditional database connections. Prisma’s default connection pooling assumes a persistent server, but in a serverless edge environment, each invocation gets a fresh runtime. Open too many connections and your database hits its limit—connection pool panic. The fix is straightforward: use Prisma Accelerate or a direct data-proxy connection that routes queries through a centralized pooler. This avoids holding open connections in the edge function itself. For smaller projects, you can also disable connection pooling entirely by setting connectionLimit: 0 in the Postgres connection string. But the real point is architectural: don’t manage pools at the edge. They don’t live long enough. Offload persistence to a middleware layer that can hold connections steady. Your edge function queries Firebase, Neon’s serverless driver, or Prisma Accelerate, not Postgres directly. That keeps cold starts fast and database connections under control.
Never instantiate PrismaClient inside an edge handler repeatedly. Use a singleton pattern or the Neon adapter shown above to reuse a single connection across invocations.
Key Takeaway
Edge functions must never manage persistent database pools—route queries through a serverless adapter or data proxy instead.
● Production incidentPOST-MORTEMseverity: high
Prisma migration drops column still referenced by tRPC procedures — silent runtime errors for 3 hours
Symptom
API endpoints returned 500 errors with 'Unknown field legacy_status'. No errors during build — TypeScript compiled because the Prisma client hadn't been regenerated on the deployment server. The deployed Prisma client was stale.
Assumption
The team assumed that running prisma migrate deploy during CI would regenerate the client. They didn't realize the build step ran before the migration step, so the old client was baked into the deployment artifact.
Root cause
The CI pipeline ran 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.
Fix
Reordered CI pipeline: (1) prisma migrate deploy, (2) prisma generate, (3) next build. Added a CI step that runs 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.
Key lesson
Always run prisma generate AFTER prisma migrate deploy in CI — the client must match the deployed schema
Add prisma generate to postinstall scripts so the client regenerates on every install
Stale Prisma client types create a false sense of safety — TypeScript compiles but runtime fails
Run prisma validate in CI before build to catch schema drift between migrations and client
Production debug guideCommon T3 Stack failures and how to diagnose them7 entries
Symptom · 01
tRPC procedure returns type error at runtime but compiles fine
→
Fix
Run prisma generate to regenerate the Prisma client. A stale client has outdated types that don't match the deployed database schema.
Symptom · 02
Server Component fetches data but the page shows 'Loading...' forever
→
Fix
Check if the component is accidentally marked 'use client'. Server Components with async/await must NOT have 'use client' — it makes them Client Components that can't use async.
Symptom · 03
tRPC query returns stale data after a mutation
→
Fix
Call trpc.useUtils().entity.invalidate() after the mutation. Without invalidation, TanStack Query's cache serves the old response.
Symptom · 04
Prisma query is slow — P99 latency above 500ms
→
Fix
Run EXPLAIN ANALYZE on the generated SQL. Check for missing indexes on foreign keys and frequently filtered columns. Add @@index in schema.prisma.
Symptom · 05
shadcn/ui component styles don't match the design system
→
Fix
Check tailwind.config.js for correct theme extension. shadcn/ui uses CSS variables defined in globals.css — verify the variable names match the component's expected tokens.
Symptom · 06
Build fails with 'Module not found' after adding a new shadcn/ui component
→
Fix
Run npx shadcn-ui@latest add [component] from the project root. The command installs dependencies and copies the component. Manual copy-paste often misses dependency declarations.
Symptom · 07
Hydration mismatch with tRPC query in Server Component
→
Fix
tRPC queries must run inside a Server Component or use a Client Component wrapper. Move the query to a Server Component or use useSuspenseQuery in a Client Component.
T3 Stack Component Comparison
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 eliminates manual API type synchronization
define a procedure once, the client gets autocompletion and caching for free.
3
shadcn/ui gives you component source code you own
no version-lock dependency, no abstraction fights.
4
Connection pooling is mandatory for serverless Prisma
without it, you exhaust database connections at ~100 concurrent users.
5
Organize by domain, not by layer
one directory per feature keeps related code together at scale.
Common mistakes to avoid
5 patterns
×
Not regenerating Prisma client after schema changes in CI
Symptom
TypeScript compiles successfully but runtime queries fail with 'Unknown field' errors. The deployed Prisma client is stale — its types don't match the actual database schema after a migration.
Fix
Reorder CI pipeline: (1) prisma migrate deploy, (2) prisma generate, (3) next build. Add prisma generate to postinstall scripts. Run prisma validate before build to catch schema drift.
×
Placing 'use client' at the top of the T3 Stack app router (e.g., dashboard/layout.tsx)
Symptom
Every component becomes a Client Component — Server Components are completely disabled. Bundle size is identical to a traditional React SPA. No RSC performance benefits.
Fix
Remove 'use client' from layout.tsx. Only mark leaf components that need useState, useEffect, or event handlers. Keep Server Components as the default.
×
Not invalidating tRPC query cache after mutations
Symptom
User creates a post, sees success message, but the post list still shows old data. The mutation succeeded but TanStack Query's cache wasn't invalidated.
Fix
Call trpc.useUtils().post.list.invalidate() after the create mutation completes. Use onSettled in useMutation options to invalidate regardless of success or failure.
×
Organizing the project by technology layer instead of domain
Symptom
Adding a feature requires touching 5+ directories (routers/, components/, types/, validations/, hooks/). Developers spend more time navigating than coding. Feature ownership is unclear.
Fix
Restructure to domain-driven: each feature gets its own directory with its router, components, and types. Shared code goes in lib/ and components/ui/.
×
Using Prisma without connection pooling in serverless
Symptom
Database connection errors at ~100 concurrent users. Each serverless function opens a new connection — the database's max_connections limit (default 100 for PostgreSQL) is exhausted.
Fix
Use Prisma Accelerate or a database-native pooler (PgBouncer, Neon's built-in pooler). Connection pooling reuses connections across function invocations.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
Explain the type chain in the T3 Stack. How does a schema change propaga...
Q02SENIOR
How does tRPC differ from REST and GraphQL? When would you choose one ov...
Q03JUNIOR
What is the difference between shadcn/ui and a traditional component lib...
Q04SENIOR
How does Prisma's edge runtime support work, and why is connection pooli...
Q05SENIOR
Walk me through the recommended CI/CD pipeline for a T3 Stack project. W...
Q01 of 05SENIOR
Explain the type chain in the T3 Stack. How does a schema change propagate type safety through the entire application?
ANSWER
The type chain starts with schema.prisma — Prisma generates a TypeScript client where every query is typed against the schema. The output of a Prisma query flows into tRPC procedures, which infer their return types. On the client, tRPC's useQuery hook infers the response type. React components receive typed props. One schema change breaks TypeScript compilation at every layer that references the changed field — catching errors at compile time instead of runtime.
Q02 of 05SENIOR
How does tRPC differ from REST and GraphQL? When would you choose one over the others?
ANSWER
tRPC provides end-to-end type inference without code generation — REST requires manual type definitions or OpenAPI codegen, GraphQL requires schema definitions and codegen. tRPC is best for TypeScript-only stacks where client and server share a codebase. REST is better for public APIs consumed by external clients. GraphQL is better when multiple client types (web, mobile, CLI) need different data shapes from the same API.
Q03 of 05JUNIOR
What is the difference between shadcn/ui and a traditional component library like MUI? Why would you choose one over the other?
ANSWER
shadcn/ui copies component source code into your project — you own and modify it. MUI ships as an npm package — you consume it as a dependency. shadcn/ui gives full customization control but requires you to maintain the components. MUI gives faster initial setup but customization fights the abstraction. Choose shadcn/ui when you need full control; choose MUI when you want a large pre-built set with minimal customization.
Q04 of 05SENIOR
How does Prisma's edge runtime support work, and why is connection pooling mandatory in serverless?
ANSWER
Prisma's traditional binary engine requires Node.js — incompatible with edge runtimes. Edge support uses driver adapters (Neon, PlanetScale, Turso) that use platform-native drivers. Connection pooling is mandatory because each serverless function opens a new database connection. Without pooling, you exhaust the database's max_connections limit (~100 for PostgreSQL) at moderate traffic. Prisma Accelerate or a database-native pooler reuses connections across invocations.
Q05 of 05SENIOR
Walk me through the recommended CI/CD pipeline for a T3 Stack project. What order should build steps run in, and why?
ANSWER
The correct order: (1) prisma migrate deploy — apply pending migrations, (2) prisma generate — regenerate the Prisma client to match the deployed schema, (3) next build — compile the application with the correct types. Running build before migrate deploy bakes a stale Prisma client into the artifact. Add prisma validate before build as a safety net to catch schema drift.
01
Explain the type chain in the T3 Stack. How does a schema change propagate type safety through the entire application?
SENIOR
02
How does tRPC differ from REST and GraphQL? When would you choose one over the others?
SENIOR
03
What is the difference between shadcn/ui and a traditional component library like MUI? Why would you choose one over the other?
JUNIOR
04
How does Prisma's edge runtime support work, and why is connection pooling mandatory in serverless?
SENIOR
05
Walk me through the recommended CI/CD pipeline for a T3 Stack project. What order should build steps run in, and why?
SENIOR
FAQ · 4 QUESTIONS
Frequently Asked Questions
01
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.
Was this helpful?
02
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.
Was this helpful?
03
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.
Was this helpful?
04
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.
Naren — Founder & Principal Engineer, TheCodeForge
20+ years building production systems in enterprise Java, banking automation, and fintech.
I built TheCodeForge because every other tutorial explains what to type
but never explains why it works — or what breaks it at 3am.
Everything here is drawn from real systems. No content mills. No AI padding.