资讯动态

T3技术栈全栈模板t3code:从技术选型到生产实践完整复盘

发布时间:2026/10/27 15:56:06 来源:尧图企业网站定制
t3code这个代号最开始只是我给自己一套全栈代码模板起的仓库名。后来发现顺着这个名字往下搜撞进的是T3技术栈那一大片社区讨论——Next.js加TypeScript加Tailwind加tRPC数据层挂一个Prisma鉴权再配NextAuth。这个组合在中小型全栈项目里出镜率越来越高核心卖点就一个让类型安全从数据库一路贯穿到浏览器。这篇文章是我从零搭起t3code到上线半年多的完整复盘包括每个选型背后的理由、关键代码怎么落、以及真正投产时踩过的那些文档里不会写的坑。适合正在调研全栈脚手架的人也适合被前后端接口类型不一致折磨到想换技术栈的人。整篇讲的不是某个商业产品是我自己基于T3技术栈沉淀的一套代码实践。如果你想快速跑一个带登录、带数据库、带全栈API的原型这里的每一步几乎都能直接抄。1. 我为什么会把t3code当项目代号技术选型复盘1.1 被接口文档折磨后的第一个念头上一份工作做内部管理系统前后端严格分离后端同学维护接口文档前端同学维护Mock数据我负责中间那层胶水代码。听起来很正规实际体验是——后端改一个字段类型文档忘了同步前端对着旧文档写了三天的页面联调时发现一半字段对不上联调阶段被产品催于是大家开始互相甩锅。最崩溃的一次一个数组字段从string[]改成object[]全项目十几个调用点因为没有类型检查报错在运行时才炸出来。我当时的结论是小团队做全栈项目再走文档驱动联调这条路就是自虐。需求明明很简单真正的成本全耗在让两个代码库理解同一个业务对象上。所以当我看到T3技术栈的核心理念——用TypeScript的推断能力把后端类型直接暴露给前端不需要接口文档不需要手写类型定义改一处全链路同步——几乎没犹豫就决定自己试试。1.2 T3技术栈六件套到各干各的什么活T3这个叫法来自社区里t3.gg那套主张封装在create-t3-app脚手架里。我的t3code项目选的是最经典的一组组合这里把分工讲清楚组件解决什么问题我理解的角色Next.js路由、SSR、API层托管整个应用的骨架TypeScript静态类型、编译期报错贯穿全链路的类型基底Tailwind CSS原子化样式避免CSS文件失控写UI不换脑子tRPC前后端函数直接调用类型端到端安全粘合剂PrismaORM、迁移、数据库访问数据层的守门员NextAuth登录会话、OAuth权限边界Tailwind在T3里的定位值得多说一句。很多人觉得它只是快速写样式但在这个组合里它的价值是让UI层不引入单独的CSS架构概念。配合TypeScript类名也可以被摇树优化和类型检查页面组件保持纯粹的数据驱动。tRPC和GraphQL都做类型安全但GraphQL需要写schema、维护resolver和query的映射tRPC直接让TypeScript的类型推断替你干这个活心智负担小一个量级。1.3 不是所有项目都适合穿这身装备t3code组起来之后陆续有同事问我能不能直接拿去用。我的回答通常是先泼冷水这套组合适合的是全栈小团队一到五个人、MVP验证、B端内部工具、以及Next.js为主的交付型项目。反过来如果你们已经有独立后端团队在维护REST或GraphQL服务前端只是消费方那tRPC这层基本用不上如果项目是给iOS和Android同时供数据的移动端业务也不会选Next.js做服务端纯展示型官网更没必要上数据库和鉴权。还有一个容易忽略的条件团队里所有人必须接受TypeScript。T3的体验建立在类型推断之上如果谁图省事到处写anytRPC的优势瞬间归零。我在t3code的README里写的第一条约定就是禁止在业务代码里出现any除非能说出三个理由。2. 用create-t3-app把t3code跑起来脚手架与目录结构2.1 初始化命令与交互选项第一步不用我重新发明轮子直接用create-t3-app官方脚手架。执行npm create t3-applatest my-t3-app交互过程会问几个选项是否用TypeScript这已经是强制的、是否启用Tailwind CSS、是否启用tRPC、数据库ORM选Prisma还是Drizzle、鉴权选不选NextAuth、import别名用默认的~/*还是改成/*。我第一次自己搭的时候看到这么多问题一度犹豫但后来发现这正是T3的好处——脚手架逼你在写第一行业务代码之前先把技术边界想清楚。需要提醒的是命令执行时要保证Node.js版本够新我建议至少18以上npm版本也是新版不然安装依赖容易有兼容性报错。这一步如果卡住大概率不是代码问题去查环境版本。2.2 生成之后必须想明白的三个问题脚手架跑完项目能启动了但我强烈不建议立刻去写页面。先用半小时想清楚下面三件事第一数据库用什么。create-t3-app默认让你配Prisma但连接串得你自己给。本地开发我建议直接用PostgreSQL的Docker容器docker run --name t3-postgres -e POSTGRES_PASSWORDdevpass -p 5432:5432 -d postgres:16然后.env里写DATABASE_URLpostgresql://postgres:devpasslocalhost:5432/t3code?schemapublic。如果只想要零配置体验SQLite也可以但生产环境我会劝你换掉。第二登录怎么接。NextAuth的Provider配置需要提前准备。我用GitHub登录时先去GitHub OAuth应用设置里把回调地址填成http://localhost:3000/api/auth/callback/github本地开发才不会白屏。接三方登录最烦的不是代码而是这些外部服务的回调配置。第三路由模式。新项目无脑选App Router。它和tRPC在Server Component里配合得很好而且Next.js官方已经把所有新功能都压在App Router上没必要逆势而为。2.3 目录结构里哪些文件是真正的主心骨脚手架生成的结构初看有点密但真正的核心文件就这几个t3code/ ├── prisma/ │ └── schema.prisma # 数据模型唯一的数据真相 ├── src/ │ ├── app/ │ │ ├── api/trpc/[trpc]/route.ts # tRPC的HTTP入口 │ │ └── layout.tsx # SessionProvider和TRPCProvider │ ├── server/ │ │ ├── api/ │ │ │ ├── root.ts # 组装所有子router │ │ │ ├── trpc.ts # context、procedure定义 │ │ │ └── routers/ # 各业务模块的接口路由 │ │ ├── auth.ts # NextAuth配置 │ │ └── db.ts # Prisma客户端单例 │ └── trpc/ │ ├── react.tsx # 前端hook和Provider │ └── server.ts # 服务端直接调用caller刚开始我的直觉是这么多封装太绕了但用了三个月后理解了这个分层的意义src/server下面全是只能跑在服务端的代码src/trpc是给前端消费的客户端桥接层src/app只负责路由和UI。边界划清楚了就不会出现服务端代码被浏览器打包进去的蠢事。src/server/db.ts里有一个细节我建议保留并说清楚因为Next.js开发模式会热重载如果不做全局单例缓存PrismaClient会被反复实例化导致连接数爆炸。脚手架生成的模板里已经有globalThis缓存方案别自己手贱改成简洁版本那是踩坑高发区。3. tRPC路由与类型安全链路t3code最核心的代码逻辑3.1 tRPC如何把调用后端变成调用本地函数tRPC的原理通俗讲是这样后端定义一堆procedure查询用query写操作用mutation每个procedure的参数和返回值都由TypeScript自动推断前端拿到router的类型定义后在组件里写出来的调用代码从函数名到参数类型到返回字段全部被编译器盯着。REST接口要维护文档和类型定义GraphQL要维护schematRPC把这三份工作压缩成了只写一份后端函数。有人担心tRPC是不是只能在Next.js里用其实它本身框架无关但当你已经选了Next.jstRPC能直接在Server Component和Server Action的体系里工作。相比手写fetch封装它省掉的不是几行代码而是整整一类接口契约同步问题。3.2 从router定义到前端useQuery的完整链路我在t3code里最常用的一块业务是文章发布先把router定义写出来感受一下类型推断的威力import { z } from zod; import { createTRPCRouter, protectedProcedure, publicProcedure } from ~/server/api/trpc; export const postRouter createTRPCRouter({ latest: publicProcedure .input(z.object({ limit: z.number().min(1).max(50).default(10) })) .query(async ({ ctx, input }) { return ctx.db.post.findMany({ take: input.limit, orderBy: { createdAt: desc }, select: { id: true, title: true, content: true, createdAt: true }, }); }), create: protectedProcedure .input(z.object({ title: z.string().min(1), content: z.string().optional() })) .mutation(async ({ ctx, input }) { return ctx.db.post.create({ data: { title: input.title, content: input.content, authorId: ctx.session.user.id, }, }); }), });前端组件里调用use client; import { api } from ~/trpc/react; const { data: posts, isLoading } api.post.latest.useQuery({ limit: 10 }); const utils api.useUtils(); const createPost api.post.create.useMutation({ onSuccess: () { utils.post.latest.invalidate(); }, });注意create用了protectedProcedure所以传入的authorId来自当前会话而不是前端表单——这是权限设计上的一个细节后面会说。两个关键收益第一前端写{ limit: 10 }时如果多传了字段或少传了必填字段编译期直接报错第二后端改了返回结构前端所有用到这个字段的地方编译器会像侦探一样把所有调用点挖出来。3.3 登录态接口protectedProcedure与中间件的正确姿势t3code里所有需要登录才能调用的接口都必须走protectedProcedure它本质上是一个中间件import { TRPCError } from trpc/server; export const protectedProcedure publicProcedure.use(({ ctx, next }) { if (!ctx.session?.user) { throw new TRPCError({ code: UNAUTHORIZED }); } return next({ ctx: { session: { ...ctx.session, user: ctx.session.user } }, }); });中间件在进入真正的业务逻辑前检查会话是否存在不存在直接抛401。这样业务代码里就不需要每个handler都写一遍判断更不会出现忘记判断登录态的低级漏洞。这里我踩过一个大坑中间件里拿不到session。排查了半天最后发现是src/server/auth.ts里NextAuth的配置没返回完整的session对象tRPC context里自然就是undefined。所以如果遇到明明登录了但接口一直401先去查auth配置和context组装而不是查中间件。另外中间件抛错时TRPCError的message不要拼上用户输入防止把内部信息通过错误详情暴露出去。4. 数据层选型t3code里的Prisma定义与迁移4.1 schema建模时的取舍Prisma在T3里承担的是数据库唯一真相的角色。我第一次建模时犯过一个典型错误把用户表里能想到的字段全塞进去结果关联关系写得一团糟。后来学乖了先只建最少字段跑通链路再加。t3code里文章模型长这样generator client { provider prisma-client-js } datasource db { provider postgresql url env(DATABASE_URL) } model User { id String id default(cuid()) name String? email String? unique posts Post[] } model Post { id String id default(cuid()) title String content String? published Boolean default(false) createdAt DateTime default(now()) updatedAt DateTime updatedAt authorId String author User relation(fields: [authorId], references: [id]) }两个容易忽略的点一是updatedAt字段Prisma会在每次更新时自动维护不用自己写时间戳二是如果用了NextAuth的数据库适配器还需要补Account、Session、VerificationToken这几张表否则登录会话写不进数据库。脚手架不会替你想清楚这些关联你得对着auth适配器的要求手动补。4.2 迁移命令与团队协作流程团队协作中数据库结构变更最容易出乱子。我的流程是先改schema.prisma再本地生成迁移文件然后代码评审时把迁移文件一起提交最后走CI时在测试环境验证。npx prisma migrate dev --name add_post这条命令会检测schema和数据库的差异生成迁移SQL并直接应用。本地没问题后生产环境用npx prisma migrate deploy这里强烈建议不要把prisma db push用在生产库上。db push是直接同步schema不经过迁移文件一旦出错改历史表结构的痕迹全丢了。migrate deploy是按顺序回放迁移团队每个人执行同一批文件数据库结构才能保证一致。4.3 查询输出控制select与include的实战差别新手最容易写出的查询是include大法为了拿文章作者名字就把整个author对象都查出来。结果前端把不该拿到的邮箱、哈希值全暴露在浏览器网络面板里既浪费流量又有隐私风险。我的习惯是查询一律显式selectreturn ctx.db.post.findMany({ where: { published: true }, select: { id: true, title: true, createdAt: true, author: { select: { name: true } }, }, });这样做的好处是第一数据库返回的数据面最小接口天然瘦第二配合tRPC的类型推断前端拿到的对象类型就是后端select出来的精确形状不会出现文档写有email实际返回没有的乌龙。如果你希望省事和安全兼顾可以定义DTO工具函数统一控制输出但项目初期一个select就够用了。5. 样式、鉴权与部署t3code从本地到线上的临门一脚5.1 Tailwind的主题化和类名组合样式层面我尽量不去碰复杂方案t3code直接用Tailwind。主题色、字体、间距都写在tailwind.config.ts里业务组件不散落硬编码颜色import type { Config } from tailwindcss; const config: Config { content: [./src/**/*.{ts,tsx}], theme: { extend: { colors: { primary: #2563eb, surface: #f8fafc, }, }, }, plugins: [], }; export default config;组件里经常需要根据条件切换类名纯字符串拼接容易出重复类名互相覆盖的问题。我建议在工具函数目录放一个cnimport { clsx, type ClassValue } from clsx; import { twMerge } from tailwind-merge; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }这个函数能自动处理tailwind类名的冲突比如同时传了p-2和p-4它会保留后者而不是两个都渲染。写动态样式统一走cn类名维护成本会低很多。5.2 NextAuth与tRPC的会话衔接登录链路的衔接很多人卡在为什么前端能看到用户信息但tRPC接口拿不到。原因通常是Provider挂载顺序或context没接对。t3code里的标准做法是两个Provider层层包住应用// src/app/layout.tsx import { TRPCReactProvider } from ~/trpc/react; import { SessionProvider } from next-auth/react; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( html langzh-CN body TRPCReactProvider SessionProvider{children}/SessionProvider /TRPCReactProvider /body /html ); }服务端这边createTRPCContext里把会话对象塞进去export const createTRPCContext async (opts: { headers: Headers }) { const session await getServerAuthSession(); return { db, session }; };这样同一份session既能在前端组件里用useSession()读也能在tRPC的context里用权限逻辑就统一了。额外提醒一下NextAuth v4和v5现在叫Auth.js的环境变量命名不一样v4是NEXTAUTH_SECRET和NEXTAUTH_URLv5是AUTH_SECRET和AUTH_URL升级时总有人漏改然后登录状态各种诡异。5.3 部署环境里最容易被忽略的几个设置部署到云平台前先检查三件事。第一环境变量要全DATABASE_URL、鉴权相关的SECRET、URL一个都不能少。第二数据库连接串要适配部署环境Serverless平台默认连接数有限直接暴露PostgreSQL端口很容易被打满建议用连接池模式比如连接串里带pgbouncer参数。第三执行npm run build的时间点TypeScript编译错误和Prisma没有generate都会在这里暴露。我个人遇到过最隐蔽的问题是Next.js的缓存。tRPC查询结果可能被Next.js路由缓存吃掉导致创建新数据后页面不刷新。解决办法是在相关页面加export const dynamic force-dynamic或者对Server Component用unstable_noStore。这些都写在文档里但只有实际被缓存坑过一次的人才会记得翻。6. t3code上线半年后的踩坑记录6.1 跑通demo和真正养一个项目之间差了七件事demo能跑起来和项目能长期维护中间隔着一片雷区。我把半年来真正踩过的坑整理成清单每一条都是文档不会主动告诉你但早晚会遇到的context依赖顺序tRPC的context组装如果依赖session那么session初始化必须先于router执行否则所有接口都拿不到session。检查顺序比检查代码更快。server-only边界src/server下所有代码默认只能在服务端运行如果被客户端组件import了打包时要么报错要么泄露数据库逻辑。我会在每个服务端模块顶部写import server-only从打包层面强制约束。数据库连接池本地连接串和生产连接串必须区分开。生产环境用连接池否则并发一上来Prisma就会报连接数超限。Next.js路由缓存创建/更新操作之后用React Query的invalidate把对应查询标记失效同时注意页面级别的缓存策略。zod校验别写两遍前端表单校验和后端tRPC入参校验共享同一份zod schema不要前端一套后端一套两套迟早会有偏差。环境变量模板化初始化项目就建一份.env.example把每个变量名和用途写清楚新成员加入三分钟就能自己配完环境。迁移流程固化生产数据库绝不手改所有变更走Prisma迁移文件变更前先备份。半年来唯一一次差点事故就是我图省事直接改了生产表字段。这七条里最让我记忆深刻的是第一条和第四条。第一次上线时好几个接口间歇性报401日志里session是undefined查了两天才发现是context的调用顺序问题后来某个列表更新后总不刷新才意识到Next.js缓存和React Query两套缓存叠加需要在正确的位置同时刷新。6.2 组件库、路由约定与模板沉淀t3code稳定运行后我开始把可以复用的部分沉淀下来。UI组件库我选了shadcn/ui它的思路不是给你一个封闭组件库而是把代码直接铺到项目里颜色和样式完全跟随Tailwind主题改起来一点不别扭。配合我之前说的cn函数组件的类名覆盖非常干净。路由约定上我固定了一套雏形公开页面走/和/post/[id]需要登录的走/dashboard管理功能走/admin并单独挂middleware.ts做守卫。业务router按模块拆post、user、billing各自一个文件root.ts只做组装。这样每次加新功能思路都是固定的不是每次重新设计。另外我会在每次踩坑后把解决方案写进项目里的docs/troubleshooting.md。这个文档现在已经成了团队真正的字典。新人遇到奇怪问题先翻它解决率比搜索引擎高很多。6.3 我的最后一公里经验先链路后样式类型安全是复利最后说一点个人体会。从我决定做t3code到第一个能登录、能发文章、能展示列表的版本跑通其实只花了一个周末。真正花时间的是后面半年的调优和踩坑修复。这个过程里我最大的感受是把一条查询链路从数据库跑到浏览器页面比把一百个页面堆出来更有价值。因为只要这条链路是类型安全的后面每加一个页面、每改一个字段编译器都会提前告诉你哪里会出错。类型安全的复利体现在哪体现在我三个月后回改一个model字段时所有受影响的前端代码都被TypeScript标了红线不需要靠测试用例去碰运气。体现在新同事入职第一天就能独立给前端加一个调用后端新接口的页面因为他不需要等接口文档IDE的自动补全就是文档。还体现在我重构目录结构时编译器第一时间揪出了所有引用旧路径的模块而不是上线后由用户来发现。现在t3code这个名字对我来说已经不止是代码模板了它代表一套工作方法先用技术选型消解沟通成本再用类型安全换取重构勇气最后用文档沉淀踩坑经验。如果你也在搭自己的全栈脚手架别急着堆功能先把那一条最核心的链路打通打通再考虑一切装饰。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑