干这行这么多年,我很少遇到一个词能把“全栈”这件事浓缩得这么准确——t3code。我第一次看到这个命名时还以为是某个培训班的名,后来接触了它背后的T3技术栈组合才发现,这四个字母基本涵盖了我日常开发里最高频、最顺手的整套工具链:Next.js、TypeScript、Tailwind CSS,再加上一个tRPC。今天这篇就想把我在t3code项目里从零搭到上线、再到踩坑修复的完整过程拆开讲一遍,尤其想聊聊那些官网文档里不会写、但实际项目里几乎一定会撞上的细节。
围绕t3code搭建一套类型安全的全栈应用,最直观的收益就是:前端调用后端接口时,不再需要手动维护一份接口文档,也不担心字段名拼错后到了运行时才炸。这个组合把类型检查的工具链贯穿到了数据库、服务端和浏览器端,我这样说可能有点抽象,但看完下面的实战过程,你会感受到它对比传统前后端分离模式的那种“省心”是实实在在的。
1. T3技术栈的底层逻辑:为什么这四个组件能凑在一起
1.1 不是拼盘,而是类型系统的一次闭环
很多第一次接触T3的人会把它理解成“四个热门技术叠在一起”,这个看法不算错,但没有说到点子上。我自己的体会是,t3code式开发的核心不在于用了谁,而在于数据的类型信息能不能从数据库一路流到UI层,中途不打折。
传统开发里,浏览器向服务端发请求,得到JSON,然后前端重新声明一遍interface,这中间极其容易脱节。而T3栈把这层墙拆掉了:Prisma根据Schema生成数据库的类型定义,tRPC在服务端直接引用这些类型定义,然后通过泛型把完整的输入输出类型暴露给前端调用方。我改一个数据模型的字段,前端编辑器里的报错立刻跟着变化,全程不需要写一行跨端的类型代码。
这种“类型自动传播”机制,让我在重构表结构时胆子大了很多。以前改字段要全局搜索调用点,现在改完Prisma Schema,编译器和编辑器会帮我把所有受影响的位置标出来,漏掉任何一个调用方都过不了构建。
1.2 Next.js在其中的角色:渲染层和API层共用一份代码仓库
Next.js在t3code体系里扮演的不只是React框架,它还承担了服务端能力。我喜欢它的一点是,路由文件和API函数可以放在同一个应用里,文件的路径本身就是一套路由。这为tRPC提供了天然的挂载点,比如我通常会在src/server/api/routers目录下维护业务路由,然后在src/app/api/trpc/[trpc]/route.ts里做一个极薄的网关层,把所有tRPC请求统一收口。
这种结构下,认证中间件、日志、错误上报都能集中在网关层处理。我用过一段时间的独立后端框架,对比下来明显感觉t3code这种同仓库模式在迭代速度上占优——你不需要在两个代码库之间来回切换,不需要单独维护一份API的SDK,改完业务逻辑前端类型直接就变了。
2. 从零初始化t3code项目:搭环境的每一步我都记了下来
2.1 create-t3-app的交互式选项里,我选了哪些配置
t3code项目的脚手架工具是create-t3-app,这条命令本身就是T3社区最值得称道的东西之一。我在终端里执行:
npx create-t3-app@latest my-t3-app接下来会出现一堆交互式选项。很多新手在这里会全选或者全不选,我的建议是不要盲目,按照你项目的实际规模来定。我自己的习惯做法是:
- Next.js:必选,版本选最新的稳定版(目前是14或15,取决于你创建时的时间点)。
- TypeScript:必选,这个不用犹豫。
- Tailwind CSS:如果项目偏内部工具或者管理后台,我建议直接选上,后面做界面效率提升明显;如果做的是重度定制化的C端设计,也可以先不加,避免后期覆盖Tailwind默认样式时麻烦。
- tRPC:这是灵魂,必选。
- Prisma:需要数据库的项目选上,它会自动生成schema和client。
- NextAuth.js:如果系统需要登录注册,选上,配合tRPC做鉴权非常顺手。
- Drizzle:如果更偏好SQL风格操作,可以用它替代Prisma,但我自己主力还是Prisma。
初始化完成后,npm run dev就能跑起来,默认在3000端口。我第一次跑起来时最惊讶的是整个项目的目录结构异常清爽,路由、服务端、样式分得清清楚楚,完全没有传统脚手架那种一层套一层的晦涩感。
2.2 项目目录里每个核心文件夹是干什么的
用create-t3-app初始化出来的项目,目录结构本身就是T3哲学的体现。我实际开发中常用的目录大概这几块:
| 目录 | 作用 | 备注 |
|---|---|---|
src/pages或src/app | 页面路由 | 新版脚手架默认用App Router |
src/server/api/trpc.ts | tRPC上下文和初始化逻辑 | 所有路由的公共入口 |
src/server/api/routers | 业务路由代码 | 按领域拆分文件 |
src/server/db.ts | Prisma客户端实例 | 全局唯一,避免连接数泄漏 |
src/trpc/shared.ts | React端的tRPC工具函数 | 客户端和服务端配置复用 |
src/trpc/server.ts | 服务端调用tRPC的封装 | 用于RSC场景 |
src/trpc/react.tsx | 客户端Provider配置 | 挂在根布局里 |
我刚开始时容易把业务代码都堆在routers下一个文件里,后来发现项目一大,这个文件会膨胀到几千行,根本没法维护。建议从第一天起就按业务域拆文件,比如userRouter.ts、projectRouter.ts、fileRouter.ts,再通过顶层appRouter合并。
3. 实战:我用t3code搭了一个带权限区的数据看板
3.1 数据库模型设计在Prisma里的落地
光讲脚手架没意思,我把之前一个客户项目的简化版拿出来拆解。这是一个带权限区分的数据看板,普通用户只能看公共数据,管理员能看用户清单和使用明细。
先在prisma/schema.prisma里定义数据模型:
model User { id String @id @default(cuid()) email String @unique name String? role UserRole @default(USER) createdAt DateTime @default(now()) accessLogs AccessLog[] } model AccessLog { id String @id @default(cuid()) userId String action String createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id]) } enum UserRole { USER ADMIN }写完后执行npx prisma migrate dev --name init,Prisma会生成迁移SQL,同时基于model里的字段生成TypeScript类型。注意,这里生成的类型是t3code全栈类型链条的最源头,它准确与否直接决定后面所有环节的类型体验。
3.2 用tRPC写业务路由:查询、变更、鉴权一次搞定
接下来是tRPC路由。我创建了src/server/api/routers/userRouter.ts,先给路由设置好受保护的状态,然后实现两个查询方法:
import { z } from "zod"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; export const userRouter = createTRPCRouter({ me: protectedProcedure.query(({ ctx }) => { return ctx.db.user.findUnique({ where: { id: ctx.session.user.id }, include: { accessLogs: true }, }); }), listUsers: protectedProcedure .input(z.object({ page: z.number().default(1) })) .query(async ({ ctx }) => { if (ctx.session.user.role !== "ADMIN") { throw new Error("需要管理员权限"); } return ctx.db.user.findMany({ skip: (page - 1) * 20, take: 20, }); }), });这段代码里的信息密度很高:
protectedProcedure会自动校验session,未登录直接拒绝,不用在每个方法里手写判断。zod做输入校验,从API入口就把脏数据挡在外面。- 管理员权限我用了一个简单的
role判断,实际更复杂的权限系统可以抽一个requireRole函数出来。
为什么说tRPC写起来爽,因为listUsers这个查询的返回类型会自动推导成User[],前端调用时如果访问user.address.city,编辑器会立刻标红,完全不需要后端再配合写一套DTO。
3.3 前端如何消费这些接口
前端这块,用tRPC生成的React hooks非常直接。我习惯在src/trpc/react.tsx里配置Provider,然后在src/app/layout.tsx里包一层:
"use client"; import { api } from "~/trpc/react"; function UserDashboard() { const { data: me, isLoading } = api.user.me.useQuery(); if (isLoading) return <div>加载中...</div>; return ( <div> <h2>你好,{me?.name ?? "访客"}</h2> <UserAccessLogs logs={me?.accessLogs ?? []} /> </div> ); }注意这里完全没有手动声明me的类型,它自动就是Prisma定义的User & { accessLogs: AccessLog[] }。我前端少写了大量的interface定义,这在老代码里是不可想象的。
4. 鉴权与权限控制:t3code项目里绝不能省的防线
4.1 NextAuth集成时最容易被忽略的回调配置
t3code默认集成的NextAuth提供了Google和GitHub等多个Provider,但实际项目中更常见的是邮箱密码登录。我需要手动搭建一套凭证认证。因为NextAuth的Credentials方案不提供默认的注册接口,需要我们自己实现一个注册的tRPC mutation。
这里有个坑我必须提醒:NextAuth的JWT回调里,如果你不把用户ID和角色手动塞进token里,那么后续的session对象里是拿不到这些信息的。我之前就栽在这个问题上,后端ctx.session.user.id一直是undefined,排查了很久才发现是回调没写。补上之后才算接对:
callbacks: { jwt({ token, user }) { if (user) { token.id = user.id; token.role = user.role; } return token; }, session({ session, token }) { if (session.user) { session.user.id = token.id as string; session.user.role = token.role as string; } return session; }, }4.2 路由级权限:管理员页面怎么锁
页面级的访问控制我会放在服务端中间件里。Next.js的middleware.ts就是干这个的:
import { withAuth } from "next-auth/middleware"; import { NextResponse } from "next/server"; export default withAuth( function middleware(req) { const token = req.nextauth.token; if (req.nextUrl.pathname.startsWith("/admin") && token?.role !== "ADMIN") { return NextResponse.redirect(new URL("/403", req.url)); } }, { callbacks: { authorized: ({ token }) => !!token, }, } ); export const config = { matcher: ["/dashboard/:path*", "/admin/:path*"], };这样HTTP层和页面路由层的权限形成了双保险,即便某个tRPC接口配错了权限,页面中间件也会兜底拦住未授权的人。
5. 跨域、RSC服务端组件和数据库连接:三个让我认真查过文档的硬骨头
5.1 Server Component里怎么安全地调tRPC
App Router推广之后,很多人会在Server Component(RSC)里直接做数据获取。项目里的src/trpc/server.ts就是为此设计的,它把服务端tRPC调用封装成一个React cache包裹的函数,避免重复请求,同时也绕过了浏览器的跨域限制。
实际使用中,在RSC里调用tRPC和客户端调用有一个很重要的区别:RSC里没有session上下文,你需要手动把cookies或者headers传给tRPC的createContext。我第一次直接在RSC里使用客户端Provider的hooks,结果报了个“MissingcreateContext”的错,后来仔细阅读了脚手架生成的server.ts才理解,createServerCaller需要显式构造。
5.2 Prisma连接池配置和并发场景的注意事项
开发模式下,Prisma每次热重载都可能堆积连接,特别是你在server/db.ts里直接导出一个Client实例时。T3文档里为此也做了专门的注释。为了保证生产环境稳定,我会在db.ts里做这样一个判断:
import { PrismaClient } from "@prisma/client"; const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined; }; export const db = globalForPrisma.prisma ?? new PrismaClient({ log: ["error", "warn"], }); if (process.env.NODE_ENV !== "production") { globalForPrisma.prisma = db; }这个全局单例模式不只是气质的提升,更是为了避免开发热更新时疯狂创建新实例导致数据库连接数爆掉。我知道这类问题不常写进博客,但它确实是线上事故的高发源头之一。
5.3 部署前必须处理的CORS和代理问题
独立前后端部署时会遇到CORS,但t3code这类同仓库部署模式天然没有同源问题。不过如果你用了自定义域名并在next.config里做了rewrites,要小心路径冲突。
我之前在Nginx里把所有/api请求转发到3000端口,结果发现/api/auth和/api/trpc两个路径在底层共用了Next.js的handlers时,偶尔会触发401。最后的解决方案是确保Nginx对/api/auth/*和/api/trpc/*采用不同的认证规则,或者全部走默认透传。
6. 前端性能优化:从响应时间到打包体积
6.1 首屏数据请求策略
t3code项目的首屏性能在默认情况下已经不错,因为RSC天然支持服务端渲染首屏部分数据。但如果你把所有数据都放在客户端useQuery里,首屏就会先请求页面骨架,再发TRPC请求。
我的做法是:首屏必须展示的核心数据(用户信息、主导航数据)直接在RSC里serverCaller调用若干tRPC查询,把数据注入给React组件;次要的交互数据(列表翻页、筛选结果)才用客户端useQuery延迟加载。
6.2 分析tRPC路由的chunk拆分
tRPC本身是打包在客户端里的,但你不能一股脑把全部路由都打包进首屏JS。t3code脚手架生成的appRouter导出是整体的,不过实际按需加载上Next.js的Tree Shaking会帮我们去掉很大一部分未使用的路由代码。如果你想更精细地控制,可以使用动态引入或者把不常用的路由函数放到serverOnly目录下,避免进入客户端bundle。
我实际优化的一个例子是,把管理员才用的auditLogRouter从主路由文件改为异步引用,结果首屏JS体积直接少了近80KB。这是非常值得做但大多数教程不会提的优化点。
7. 从开发到上线:我学到的三个关于t3code的实用经验
第一个经验是保持Prisma schema单一来源。t3code的一切类型推导都源自数据库模型,如果你手动在服务端重新定义一份类型,就等于打破了链条,前端的类型安全也会跟着失效。我见过有人为了兼容旧接口,在tRPC的output上做了一层映射,短时间内看着挺好,时间一长类型和实际数据就不对上了,排查起来比不做类型安全还痛苦。
第二个经验是把tRPC的错误处理做成统一格式。比如后端抛出TRPCError时,可以带一个code枚举(UNAUTHORIZED、NOT_FOUND等),前端收到统一错误对象后按code渲染对应的Toast或跳转。我在项目里封装了一个通用的ApiErrorMessage组件,之后所有页面的错误处理逻辑都收敛到了一个地方,维护成本下降很多。
第三个经验是自动化测试要盯住tRPC路由这个层面。t3code的架构天然适合写集成测试:起一个测试数据库,通过createCaller直接调用router,不需要启动HTTP服务,也不需要mock网络层,测起来非常快。我在项目的CI里加了几个关键路径的测试,比如“未登录调用受保护接口必须抛错”“管理员才能看到用户清单”,这几个用例成了防止权限回归的兜底。
8. 踩过的坑和填坑思路:如果你正卡在相似问题上
8.1 tRPC类型推导失效,前端拿不到类型
出现这个问题先别急着怀疑框架。我最常见的原因是两个:一个是没有在根布局正确挂载<TRPCReactProvider>,另一个是编辑器没有重启导致TS Server缓存没刷新。前者改一行布局文件就能解决,后者按Ctrl+Shift+P找到“TypeScript: Restart TS Server”即可。
8.2 Prisma migrate和数据库不一致,导致启动报错
开发过程中改动schema后,如果忘记执行prisma migrate dev,tRPC调用查询时会遇到表不存在或列不匹配的错误。很多新手跑起来后报错,第一反应是改tRPC路由,其实真正的修复在数据库迁移命令。养成习惯:每改一次schema,立刻执行npx prisma migrate dev --name describe_your_change。
8.3 NextAuth回调里secret不一致导致登录闪退
生产环境如果设置NEXTAUTH_SECRET和本地不一致,用户登录时会有间歇性跳回登录页的问题。这个坑排查的时候很难发现,因为本地不设置secret也能跑通。解决办法很简单:生产部署前必须显式设置一个稳定的随机字符串作为NEXTAUTH_SECRET,而且保持所有环境变量一致。
8.4 热更新过程中Tailwind样式丢失
这个问题更多发生在项目规模变大后。热更新偶尔不重载Tailwind的生成层,这时只需要在命令行里执行一次kill掉dev server并重启,90%的情况能恢复正常。如果还不行,可以检查tailwind.config里content字段是否覆盖了新增的页面目录。
结尾
t3code这套技术栈,真正让我产生依赖感的不是某单个库的API有多好用,而是它把全栈类型安全变成了一种“默认状态”——我不需要刻意去维护,就能自然地避开一大类运行时错误。在实际项目里,它帮我省下了大量联调时间,也把很多潜在的数据安全问题消灭在编译阶段。
最后分享一个小操作:在t3code项目里,我给团队约定了一条底线规则——任何router函数都必须明确使用publicProcedure或protectedProcedure,不允许自定义一个绕过鉴权的procedure。这个约定看起来简单,但有效避免了好几回权限漏洞的出现。如果你正在考虑把新项目切到这套组合上,我的建议是直接上,尤其是在中后台这类业务逻辑重、接口多的场景里,t3code的收益会随着项目复杂度的增长越来越明显。