t3code全栈脚手架实战:Next.js+tRPC+Prisma从零到部署

发布时间:2026/10/28 22:48:22
t3code全栈脚手架实战:Next.js+tRPC+Prisma从零到部署 做全栈项目这几年我最大的体会是真正拖慢交付进度的往往不是业务逻辑本身而是前后端之间那些琐碎又重复的衔接成本。接口文档要维护类型定义要双写鉴权逻辑要反复粘贴数据库表结构改一次就得全项目通报一次。t3code这个名字从工程实践角度看就是针对这一堆内耗给出的答案把Next.js、TypeScript、tRPC、Tailwind这套T3技术栈封装成一条命令就能拉起的全栈骨架自带数据库、认证和类型安全的API层。刚上手的人可能觉得这不就是脚手架嘛但真正用起来会发现它把很多团队要花一周才能搭好的基础设施在几分钟内梳理得干干净净。我最近用这套流程交付了一个内容管理类的小工具从初始化到线上运行只花了一个周末。这篇文章打算把从零搭建到部署的完整过程、关键配置、以及我踩过的一些坑按实际操作顺序讲清楚。想快速落地产品原型或者不想在前端后端分离的泥潭里反复折腾的朋友可以参考这份实践。需要说明的是我这边使用的t3code命令是对社区T3生态create-t3-app的封装交互逻辑一致下面所有操作对两者都适用。1. 项目定位t3code 解决的全栈开发内耗1.1 全栈项目最容易失控的三种“隐性成本”在正式介绍流程之前先把问题定义清楚。这几年我和团队经手过不少全栈项目回头看复盘发现真正无效的投入集中在三块。第一是API文档维护成本。前端一个字段命名后端一个字段命名接口文档靠手写Markdown或者 Postman 维护。字段一多文档永远跟代码脱节前端照着文档调后端看着代码写联调时经常发现“这个接口根本没这个参数”来回拉扯半天。第二是类型不互通。这个更常见。前端定义User类型后端也定义User类型数据库表又是另一种结构。一次改表开发群里连发三遍截图仍然有人忘记同步。等接口返回多了驼峰下划线转换或者嵌套对象维度变化运行时才突然暴露undefined排错成本比写代码本身还高。第三是基建代码重复劳动。登录鉴权、数据库连接池、请求错误处理、跨域配置这些代码几乎每个项目都要写一遍。写完还没人觉得有价值因为它的作用是“让业务代码可以安全地跑起来”而不是业务本身。但没有它项目寸步难行。这三个内耗在项目规模小的时候可以用个人记忆力弥补一旦业务复杂化任何一点遗漏都会变成生产事故。T3这套组合解决的方向很直接把类型作为唯一的沟通语言把基建封装成工程模板。1.2 T3组合的选型逻辑为什么是Next.js、tRPC和PrismaT3的全称对应四件套——TypeScript、Tailwind、tRPC第三个T在不同版本里有所变化。但真正让我决定长期用它的是三个关键设计。Next.js负责前端和路由层SSR/SSG能解决大部分页面渲染需求App Router又让API Routes和页面共处在同一个项目里。这意味着前后端不用分成两个仓库部署一份代码、一套依赖部署链路直接砍半。tRPC把一个核心问题解决得很漂亮不用写REST路由、不用维护OpenAPI文档只要定义一份router前端调用时TypeScript会自动推断入参和出参类型。接口改动会直接变成编译期错误而不是联调现场才暴露的运行时问题。Prisma把数据库建模和业务代码绑定在同一套类型体系里。表结构定义在schema.prisma生成出的client类型和tRPC上下文天然相通。一句话总结这套技术栈的原则是“能由编译器保证的就不放到运行时去碰运气”。t3code做的就是把这一整套原则落实到工程初始化阶段。有的朋友会问为什么不直接用Spring Boot或者NestJS这不也可以类型安全区别在于对那些以页面和产品体验为主的团队来说Node技术栈可以让前端团队全员参与全栈开发不需要额外引入一支后端团队。技术选型永远不只是技术问题也是团队结构问题这是我这两年越来越深的体会。2. 工程初始化命令、交互式选项与目录结构2.1 环境准备与初始化命令进入实操阶段。先说环境我建议使用Node.js 20 LTS或以上版本依赖和项目从Node 18.18开始才能比较完整地兼容新的Next.js 15和Prisma 6系列。如果你本机同时装了多个Node版本用nvm或fnm按目录切换远比直接整机升级稳妥。我这边用到的初始化命令如下npx t3codelatest create my-app cd my-app如果你是直接从社区生态拉取等价命令是npx create-t3-applatest my-app下面流程完全一致。命令执行后会进入交互式问答主要问题包括选择包管理器、勾选需要的功能模块。我的建议是所有模块全选NextAuth、Prisma、Tailwind、tRPC、App Router全覆盖。这里有一个小经验如果项目前期不确定要不要认证或数据库宁可先勾上再后期移除也不要一开始图省事。因为这些模块在工程里是互相咬合的比如tRPC的protectedProcedure依赖NextAuth的sessionPrisma的client又要注入tRPC的context中途补装往往比初始化时一并安装更折腾。包管理器上npm、pnpm、yarn都能跑但我个人在团队项目里统一用pnpm。原因很实际安装速度快、磁盘占用小更重要的是pnpm的依赖隔离机制能提前暴露一些“隐式依赖”的问题这类问题在部署阶段最容易踩雷。不是性能洁癖是真的能少填几个坑。2.2 工程结构初始化完成后的目录地图初始化完成后打开工程根目录会看到一套相对固定的结构my-app/ ├─ prisma/ ├─ public/ ├─ src/ │ ├─ app/ │ ├─ server/ │ ├─ trpc/ │ ├─ env/ │ └─ styles/ ├─ .env ├─ package.json └─ tsconfig.json我逐个解释下核心区域prisma/目录下面就是schema.prisma整个数据库层都从这一个文件开始。写模型、定义枚举、指定数据库provider都在这里完成。很多人第一次看到这个目录会忽略它实际上后面所有表结构变更都离不开它。src/server/api/是后端路由的主要阵地。trpc.ts负责初始化tRPC实例并定义上下文routers/目录下按业务放各种router文件。src/trpc/是客户端和服务端共享的类型入口前端调用接口所用的hooks也由这里导出。src/app/是Next.js App Router的页面区同时API Route也放在这里的api/子目录下。env.mjs或src/env目录用于编译期环境变量校验这是整个工程里最容易被人忽视、也最容易出问题的文件后面我会专门讲。看懂这个结构只需要十分钟但对后续定位问题帮助非常大。我见过不少朋友拿到工程后直接把代码堆到src/app/page.tsx里结果业务一多页面文件膨胀到上千行后面根本没法维护。骨架的价值在于它提前替你划分好了“页面、服务、接口、数据模型”的边界顺着边界写代码项目规模翻倍也不会乱。2.3 环境变量启动之前先补齐这三样脚手架生成的.env和.env.example里已经列了需要的变量但初始化问答阶段不会替你填具体值。最常见的启动失败十有八九都跟环境变量缺失有关。我建议启动前先补齐三样东西。第一数据库连接串DATABASE_URL。如果你本地没有装数据库先起一个Postgres实例最简单的办法是Docker跑一个容器docker run --name postgres-dev -e POSTGRES_PASSWORDdevpass -p 5432:5432 -d postgres:16对应的DATABASE_URL类似postgresql://postgres:devpasslocalhost:5432/postgres。第二认证密钥AUTH_SECRET老版本也叫NEXTAUTH_SECRET。这个值用openssl命令生成openssl rand -base64 32第三如果启用了第三方登录Provider比如GitHub OAuth那么GITHUB_CLIENT_ID和GITHUB_CLIENT_SECRET也要填。开发阶段可以先去GitHub的Developer settings OAuth Apps里注册一个本地回环应用回调地址填http://localhost:3000/api/auth/callback/github。顺手提一句.env.example是提交到代码仓库的模板.env则必须写进.gitignore。我在代码评审里看过不止一次把真实密钥提交到Git仓库的情况这个习惯特别危险。密钥一旦泄露到公开仓库即使马上删除提交历史记录里也还在唯一的处理办法就是立即吊销并重新生成。3. 一条链路实战Prisma建模、NextAuth登录与tRPC接口3.1 定义数据模型并完成首次迁移现在进入真实业务。我以一个小型内容系统为例需要用户表和文章表用户登录后可以创建文章文章列表展示作者信息。先在prisma/schema.prisma里加模型。model User { id String id default(cuid()) name String? email String? unique password String? posts Post[] accounts Account[] sessions Session[] } model Post { id String id default(cuid()) title String content String author User relation(fields: [authorId], references: [id]) authorId String createdAt DateTime default(now()) updatedAt DateTime updatedAt }保存后执行一次迁移npx prisma migrate dev --name init这里说明一下prisma migrate dev会做三件事比较schema和数据库的实际状态、生成迁移SQL文件、并执行到数据库。如果输出窗口没有报错接下来用npx prisma studio打开一个本地可视化界面可以直接查看表数据。初次跑迁移容易忽略一个细节schema里User模型通常已经由脚手架自带千万不要手改id字段的类型或default策略否则会跟NextAuth的PrismaAdapter对不上。认证相关的Account、Session、VerificationToken模型脚手架也会默认生成好这些模型的结构建议保持原样因为它们由NextAuth规范约束。3.2 NextAuth登录接入把session变成全栈共享状态脚手架默认集成了NextAuth的逻辑开发阶段最省事的接入方式是先配GitHub Provider跑通全流程。实现上在src/server/auth.ts里做两件事配置providers定义callbacks。核心代码大致如下import NextAuth from next-auth; import GitHub from next-auth/providers/github; import { PrismaAdapter } from next-auth/prisma-adapter; import { db } from ~/server/db; export const { handlers, auth } NextAuth({ adapter: PrismaAdapter(db), providers: [GitHub], callbacks: { session: ({ session, user }) ({ ...session, user: { ...session.user, id: user.id }, }), }, });登录相关的API Route位于src/app/api/auth/[...nextauth]/route.tsNextAuth会自动处理OAuth回调、session写入和cookie管理。前端页面上可以直接调用signIn和signOut方法一个按钮就能完成登录注销。这里要特别强调一个版本差异老版本NextAuth v4的写法是getServerSession(req, res, authOptions)而新版本已经变成直接调用auth()函数获取session。如果你在搜索引擎上复制到v4的代码直接粘贴到新工程里很可能报错。官方迁移指南主要就是把导出方式和API调用方式对齐建议升级前先读一遍。我在这上面浪费过整整一个下午。开发环境下NextAuth默认要求安全密钥没有正确设置时启动后会一直报错错误信息是MissingSecret。虽然某些文档会说可以用一个固定字符串顶一下但生产环境千万不要这么干。密钥应该作为环境变量注入长度建议32字节以上避免用太短的弱口令。3.3 tRPC接口一份router接收前端请求认证接好后开始定义业务接口。先看src/server/api/trpc.ts里是怎么初始化的。import { initTRPC, TRPCError } from trpc/server; import { auth } from ~/server/auth; import { db } from ~/server/db; export const createTRPCContext async (opts: { headers: Headers }) { const session await auth(); return { session, db, headers: opts.headers }; }; const t initTRPC.contexttypeof createTRPCContext().create(); export const createTRPCRouter t.router; export const publicProcedure t.procedure; export const protectedProcedure t.procedure.use(({ ctx, next }) { if (!ctx.session?.user) { throw new TRPCError({ code: UNAUTHORIZED }); } return next({ ctx: { ...ctx, session: { ...ctx.session, user: ctx.session.user } }, }); });这段代码里最重要的是protectedProcedure它是一个中间件先检查ctx.session是否存在不存在就直接抛UNAUTHORIZED。这样“未登录不能访问”这个约束就不用每个接口都自己写一遍。接着在src/server/api/routers/post.ts里定义文章相关接口。import { z } from zod; import { createTRPCRouter, protectedProcedure } from ~/server/api/trpc; export const postRouter createTRPCRouter({ create: protectedProcedure .input(z.object({ title: z.string().min(1, 标题不能为空), content: z.string().min(1), })) .mutation(async ({ ctx, input }) { return ctx.db.post.create({ data: { title: input.title, content: input.content, authorId: ctx.session.user.id, }, }); }), list: protectedProcedure.query(async ({ ctx }) { return ctx.db.post.findMany({ orderBy: { createdAt: desc }, include: { author: { select: { name: true } } }, }); }), });最后把postRouter挂到根路由src/server/api/routers/root.ts并把appRouter导出给前端使用。这样一套接口链路就算完成了。实际操作里zod和tRPC的组合是最舒服的部分之一通过.input()定义zod schema前端调用时会自动获得入参类型提示非法参数在编译期或者请求进入服务端后立即被拦截。不需要手动写参数校验逻辑也不用在controller层做一堆if-else。3.4 前端调用类型提示直接来自服务端前端这边的核心在src/trpc/react.tsx。这个文件导出了一个api对象它是createTRPCReact的实例使得客户端能感知服务端所有router的类型。use client; import { useState } from react; import { QueryClient, QueryClientProvider } from tanstack/react-query; import { httpBatchLink } from trpc/react-query; import { createTRPCReact } from trpc/react-query; import { type AppRouter } from ~/server/api/routers/root; const api createTRPCReactAppRouter(); export function TRPCReactProvider(props: { children: React.ReactNode }) { const [queryClient] useState(() new QueryClient()); const [trpcClient] useState(() api.createClient({ links: [httpBatchLink({ url: /api/trpc })], }) ); return ( api.Provider client{trpcClient} queryClient{queryClient} QueryClientProvider client{queryClient} {props.children} /QueryClientProvider /api.Provider ); }然后在任意组件里就可以这样调用use client; import { api } from ~/trpc/react; export function CreatePostForm() { const utils api.useUtils(); const createPost api.post.create.useMutation({ onSuccess: () utils.post.list.invalidate(), }); return ( form onSubmit{(e) { e.preventDefault(); createPost.mutate({ title: test, content: content }); }} button typesubmit创建文章/button /form ); }几个要点说一下。第一调用api.post.create时IDE会直接提示需要title和content字段而且类型来自服务端zod schema。第二useMutation之后通常要配合invalidate去刷新对应query的缓存否则创建完列表不会自动更新。第三Provider必须挂在客户端组件的顶层而且一个项目里只能有一个api实例注意不要在多个地方重复createTRPCReact。这套流程我跑过很多次第一次接触的人最常犯的错就是把事件handler写在服务端组件里导致请求从未发出。React Server Components和tRPC的交互方式跟传统React不同需要习惯用“use client”标记交互组件。4. 高频报错排查环境变量、迁移与版本兼容4.1 项目跑不起来先检查环境变量很多新手启动时报错第一反应是去改代码其实大部分问题出在配置上。这里写一个我自己的排查顺序效率会高很多。第一类错误信息包含Environment variable not found: DATABASE_URL。这是Prisma的客户端在启动时读不到环境变量。先确认.env文件在项目根目录、并且拼写一致再确认数据库真的在监听。本地用Docker起的Postgres容器如果前一天关机后没启动端口一定是断的后面所有跟Prisma相关的接口都会超时。第二类错误信息包含MissingSecret或NEXTAUTH_SECRET is missing。对照上一节说的生成密钥并写入.env然后必须重启进程。这里有个特别容易忽略的点很多脚手架会在dev模式下带watch自动重载但.env的变更并不一定能触发重启。dotenv类工具通常在进程启动时读取一次。改完.env后手动CtrlC再重新npm run dev能省掉大量怀疑人生时间。第三类登录第三方Provider时出现redirect_uri错误。这个基本是OAuth应用里配置的回调地址跟当前环境不一致。开发环境回调地址通常是http://localhost:3000/api/auth/callback/github生产环境则要对应线上域名。如果本地用了http协议而OAuth只允许https也会被拒。我的习惯是一开始就把dev和prod环境变量分开而不是每次切换环境都去改GitHub后台。这类问题有一个通用心法看日志时先区分“配置错误”和“代码错误”。配置错误往往在启动阶段就报错代码错误更多发生在请求到达业务逻辑之后。不要启动一报错就一头扎进组件代码。4.2 Prisma迁移改表导致的连锁反应Prisma的兜底机制很好用但也有些问题几乎每个团队都遇到过。第一个是修改schema后忘记执行迁移。本地跑的时候Prisma Client认为表结构是generate那一刻的样子。如果改了schema而没有重新迁移并重启服务业务里访问新字段就会直接报column does not exist。建议形成习惯每次改schema后的固定三连——npx prisma migrate dev --name xxxx、npx prisma generate、重启dev server。第二个是删除字段或重命名表导致的外键问题。比如删除一个用户时如果用户表跟文章表之间存在外键关联Prisma默认不会帮你做级联删除。删除用户时会抛Constraint Violation。解决方式是在schema里显式声明onDelete: Cascade或者在业务代码里先删关联数据。第三个比较冷门但挺常见多个人同时改schema迁移文件产生冲突。A本地创建的migration文件名跟B重复prisma migrate dev会把它当成数据库已有记录从而拒绝执行。处理方式一般是把冲突的migration文件删掉重新生成或者基于分支合并后手动确认。迁移问题的共性就是“状态不同步”本地、CI、生产三个环境之间尤其明显。尽量让迁移的执行动作由CI管道统一控制而不是靠每个开发在本地凭记忆执行。4.3 版本升级Next.js 15时代最容易出现的兼容问题脚手架初始化出来的版本是相对固定的但你想升级某些依赖时兼容性问题就会暴露出来。以Next.js 15为例几个明显变化。第一cookies()、headers()这些API变成了异步函数如果你沿用Next.js 14的模板代码const token cookies()会直接被TypeScript标红。第二useSearchParams在静态渲染页面里要求外层包Suspense否则构建直接报错。第三NextAuth v5对Next.js 15的支持路径跟v4不同建议直接使用新版提供的auth()函数而不是老旧的getServerSession写法。React的升级也是大坑。React 18到React 19的过渡中许多第三方库会存在peerDependencies冲突。npm安装时报ERESOLVE是很常见的这时候不要用--force强行装。正确做法是先升级最上游的框架依赖再让其他包适配。或者干脆锁版本不追新生产稳定性永远比版本新鲜感重要。排查版本问题时我比较推荐用npm explain pkg或者pnpm why pkg看依赖关系树看看同一个包是不是被解析出了多个版本。类型冲突经常来自这种双版本并存情况比如两个tRPC实例分别引了不同版本就会出现“Type instantiation is excessively deep”之类的诡异报错。5. 部署上线从本地环境搬到生产的避坑指南5.1 上线前四个必需检查项本地跑通只是第一步部署上去才能真正见用户。我每次发布前会按清单检查避免低级错误。一是数据库。生产环境优先使用云数据库或托管型Postgres连接串里一般要带SSL参数比如加上?sslmoderequire。把DATABASE_URL替换成生产值后先跑一次迁移。注意生产迁移不建议用migrate dev应该用prisma migrate deploy前者会尝试重置开发库后者才是执行已生成迁移文件的正确姿势。二是认证密钥与OAuth回调。生产环境会用到一套完全不同的密钥和域名确保环境彻底隔离。特别是NextAuth在多个实例部署时session cookie签名依赖同一个密钥如果每台机器随机生成一个密钥用户登录状态就会在各实例间漂移。这个问题只有在多实例或Serverless冷启动情况下才明显很容易被忽略。三是环境变量隔离。现在很多项目都有.env.development和.env.production的概念但部署平台上还是要到控制台重新填一份完整配置。有个小习惯我一直在用在.env.example里每个变量都写一行注释说明是什么、在哪里获取、属于dev还是prod这样每次部署新环境只需要照单填一遍。四是静态资源与脚本。如果项目里有自定义的构建脚本比如生成sitemap或同步静态数据一定要确认这些脚本在CI上也会执行而不是只存在于本地记忆里。5.2 构建流程让prisma generate跑在正确时机部署到Serverless平台时大家最容易忽略的是Prisma的生成时机。Prisma Client不会自动打包进去必须在构建阶段执行prisma generate。一般的做法是在package.json里加一个postinstall脚本{ scripts: { postinstall: prisma generate } }这样无论npm install发生在本地、CI还是云平台都会自动生成client。数据库迁移则不建议放在构建阶段因为多实例并发部署时两个实例同时跑migrate可能互相冲突。更稳妥的方案是在CI里单独抽一个步骤执行prisma migrate deploy构建流程只负责generate和build。到这一步整个从零搭建到上线的流程就跑得差不多了。最后分享一个我自己的习惯每次用脚手架初始化完项目我都会先提交一次空commit记录下工程生成时的版本号和选项。之后升级依赖或者排查兼容性问题时可以很清楚知道是从哪个基线改动的。这个习惯很小但在团队协作里帮我定位过好几次环境差异问题。