YAOTU INSIGHTS

t3code全栈脚手架:Next.js与tRPC驱动的类型安全项目模板

t3code全栈脚手架:Next.js与tRPC驱动的类型安全项目模板
1. 项目缘起与整体设计思路1.1 为什么叫t3code一个被缩写逼出来的全栈模板“t3code”这个名字我最初是想注册一个更长的域名来着结果发现好名字基本都被抢光了。最后干脆用了我日常开发里最顺手的三个技术首字母——TypeScript、Tailwind CSS、tRPC——再加上code拼成了t3code。说实话这个项目最初只是为了我自己干活偷懒用的结果做完之后发现很多前端朋友因为这套组合确实能省下大量重复造轮子的时间。先说清楚这个概念免得有人误解。t3code不是一个新的框架也不是某种需要重学的范式。它是我基于Next.js 14 App Router、TypeScript严格模式、tRPC、Prisma、Tailwind CSS和shadcn/ui组件库封装的一套完整全栈项目脚手架。简单点说就是把我这几年前后端分离、又合并、再分离折腾下来的最佳实践固化成了一个开箱即用的模板。这个项目要解决的问题很直接第一新项目启动时环境配置、目录结构、代码规范、请求链路这些基础设施每次都要重搭一遍既枯燥又容易出错第二全栈项目往往在前端调用后端接口时类型定义要靠手写维护后端改了字段名前端跑起来才知道碰上线就尴尬第三React生态的组件库虽然多但真正能让团队统一风格、支持深浅色主题无缝切换的方案选起来也费劲。t3code把这三个坑一次性填平了。我自己估算过用t3code初始化一个带用户体系、权限控制、后台管理布局的新项目从零开始大概要两天半的工作量用模板十分钟就能跑起来后面只需要专注业务代码。如果你正好在找一套能直接抄作业的全栈起步方案或者想把现有的React项目重构得更有条理这篇文章应该对你很有帮助。1.2 技术选型的核心逻辑为什么不是Next.js配Express也不是Vite配Nest做技术选型时项目群里经常争得面红耳赤。我的原则很简单优先考虑团队维护成本和类型安全覆盖度其次才是性能和生态热度。t3code最终选了Next.js App Router而非Vite配Nest核心原因是Next.js把前后端放进了同一个工程目录部署时一个项目搞定既不需要单独维护两个仓库也不需要为跨域问题反复配置代理。而选tRPC而不是REST或者GraphQL这件事我确实犹豫过一段时间。tRPC的最大特点是前端调用后端函数就像是调用本地函数一样而且类型是直接从后端推导到前端的。实际体验就是后端API签名改了前端编译直接报错根本轮不到运行时才发现。这比手写接口文档和类型定义靠谱得多尤其适合中小团队。当然tRPC不太适合完全对外的开放API如果你需要给别人提供公共接口那还是REST更稳。数据库选Prisma主要是看中它的schema声明式建模和迁移流程可控。Prisma生成的client带完整类型和tRPC搭配起来从数据库表到前端组件类型一路畅通无阻。Tailwind CSS加shadcn/ui的组合呢胜在可控性好可以自定义设计令牌也能直接复制组件源码改不会像传统UI库那样被封装得死死的。这套技术栈还有个隐形优势——它把“类型安全是团队协作的润滑剂”这个概念发挥到了极致。每个人改完后端模型前端队友拿到的是编译器的提示而不是模棱两可的沟通这个体验一旦习惯就回不去了。2. 脚手架搭建与工程化配置2.1 初始化t3code一条命令跑通开发环境先说我实际用下来的初始化流程。如果你手头有Node.js 18.17以上版本可以直接用degit把模板拉下来速度快而且不会保留多余的历史记录。命令很简单npx degit your-name/t3code my-project cd my-project npm install装依赖这一步稍微等一下因为Next.js和Prisma的依赖树比较大。装完以后核心动作是初始化环境变量。项目里已经放了一个.env.example你需要复制成.env然后填上数据库连接串。我最常用的是SQLite先跑开发环境零配置改一行就行DATABASE_URLfile:./dev.db这个优点对新手特别友好不用先装MySQL或者PostgreSQL就能把项目跑起来等确定要部署了再迁到PostgreSQL也不迟。接下来同步数据库结构npx prisma migrate dev --name init这一步会在数据库里创建对应的表同时生成Prisma Client。然后启动开发服务器npm run dev浏览器打开localhost:3000你就能看到带登录注册、深色模式切换、一个示例CRUD页面的完整壳子了。走到这一步环境就算搭好了。2.2 目录结构每一层该放什么不该放什么t3code的目录结构是我反复调整过好几轮的参考了很多开源项目的沉淀。核心思路是按能力分层而不是按类型分层。也就是说不能简单粗暴地搞一个components文件夹把所有组件都扔进去那样项目大了以后找东西全靠脑补。实际结构大致这样src/ app/ (auth)/login/page.tsx (dashboard)/admin/page.tsx api/trpc/[trpc]/route.ts layout.tsx components/ ui/ # shadcn生成的基础组件 forms/ # 表单类业务组件 layout/ # 导航、侧边栏、头部 server/ db/ api/routers/ # tRPC业务路由 auth.ts trpc/ client.ts server.ts lib/ utils.ts validations/app目录是页面层只放路由组件和页面骨架不在里面写业务逻辑。components分层放UI组件和业务组件server放数据库和tRPC路由trpc负责创建客户端和服务端的调用封装。lib下放工具函数和 zod 校验规则。这种分层的价值等到项目有几十个页面、十几个路由时最能体现出来。新同事入职看这个结构基本不用太多口头讲解就能知道什么东西该往哪放。我踩过的坑是早期把所有组件都堆在components根目录下结果一个月后一个按钮组件出现了五个风格迥异的版本非常头疼。2.3 TypeScript严格模式与ESLint配置别把警告留给同事TypeScript的strict: true是t3code的默认配置而且是强制不关闭的。很多人觉得严格模式碍事总想着把strict改成false图省事但我强烈建议你硬着头皮过这一关。严格模式逼着你把null和undefined的边界想清楚写出来的代码运行时的意外会直线下降。t3code的tsconfig里还开了noUncheckedIndexedAccess这意味着访问数组元素时TypeScript会提醒你结果可能是undefined这虽然烦人但真的能拦住一大批运行时错误。ESLint和Prettier用的是antfu的配置风格这个风格集合挺合我胃口没有分号、函数名和括号之间留空格、排序导入语句。你可能一开始不习惯但格式化统一之后PR里的代码差异会干净很多。我团队里面真正让我省心的是配置了lint-staged和husky在提交前自动跑检查和格式化npx husky-init npm i -D lint-staged然后修改.lintstagedrc{ *.{ts,tsx}: [eslint --fix, prettier --write], *.{json,md}: [prettier --write] }每次git commit时只对改动的文件做检查速度非常快不用等全项目lint完。这个体验加进去以后团队提交代码的通过率明显提高。3. 核心业务实现与关键环节3.1 tRPC请求链路从数据库到UI的类型穿透t3code最核心的亮点就是把tRPC的“类型穿透”玩明白了。用一个例子来理解这个机制假设你有一个获取用户列表的接口传统REST的做法是后端写一个GET /api/users返回JSON数组前端再手动定义一个User[]接口。问题在于一旦后端把nickname改成name前端代码不会报错只有运行时发现数据全是undefined才慌神。在t3code里tRPC路由定义在server/api/routers/user.ts中大概是这样的import { z } from zod; import { createTRPCRouter, publicProcedure } from ../trpc; import { db } from ~/server/db; export const userRouter createTRPCRouter({ list: publicProcedure .input(z.object({ page: z.number().int().default(1) })) .query(async ({ input }) { const users await db.user.findMany({ skip: (input.page - 1) * 20, take: 20, }); return users; }), });前端调用时完全不需要手动声明响应类型import { api } from ~/trpc/client; const { data: users, refetch } api.user.list.useQuery({ page: 1 });这里users自动就是User[]类型字段变更会直接在编辑器里标红。前后端类型的一致性完全由编译器保障这就是tRPC比手写REST endpoint强的核心原因。而zod校验也藏在过程里每个路由的input都要求用zod定义好结构不符合直接拒绝执行。也就是说请求在进入业务逻辑之前就已经被验证过了不需要在函数内部再写一堆手动判断字段是否合法的代码。3.2 数据库模型设计与Prisma迁移字段名改了就别想瞒着前端Prisma的schema文件是t3code里数据模型的唯一事实来源。我为了演示和实际业务的通用性内置了User、Account、Session三张表这是典型的认证相关架构。很多教程喜欢用username和password做演示但那套体系真实项目里几乎没人用主要还是OAuth或是邮箱密码加session。schema中一个典型的模型长这样model User { id String id default(cuid()) name String? email String unique emailVerified DateTime? image String? passwordHash String? role Role default(USER) created_at DateTime default(now()) updated_at DateTime updatedAt accounts Account[] sessions Session[] } enum Role { USER ADMIN }这里有个小心思用cuid()而不是自增id。因为分布式部署时自增主键容易撞车而且cuid在URL里也不会暴露数据总量安全性更好一点。改模型后执行npx prisma migrate dev --name your_change迁移脚本会生成并执行同时Prisma Client类型会自动更新。如果在JavaScript代码里用了不存在的字段编辑器和next build都会立即报错这个约束让我在上线前就规避了大量字段不一致的问题。3.3 NextAuth鉴权与权限控制从登录到中间件守卫t3code的登录集成的是NextAuth也就是Auth.js。配置在src/server/auth.ts里支持邮箱密码登录也支持GitHub OAuth和Google OAuth。我觉得对国内项目来说邮箱登录加一个简单的验证码流程是常规操作t3code里做了个简化版但提供了可以直接接短信或邮箱验证码的扩展点。权限控制用中间件实现。Next.js的middleware可以在请求进入页面之前拦截t3code里写了一个很实用的逻辑export { default } from next-auth/middleware; export const config { matcher: [/admin/:path*, /dashboard/:path*], };这样未登录用户访问后台相关页面会直接被重定向到登录页不用在页面里手动判断session。需要做细粒度权限控制的地方比如只有ADMIN角色能访问的管理员页t3code的做法是在页面组件里加一个服务端校验import { redirect } from next/navigation; import { getServerSession } from next-auth; import { authOptions } from ~/server/auth; export default async function AdminPage() { const session await getServerSession(authOptions); if (session?.user?.role ! ADMIN) { redirect(/403); } // ... }这个比纯客户端判断可靠得多因为真正的鉴权必须发生在服务端前端跳转只是用户体验层的事。3.4 Tailwind CSS与shadcn/ui主题定制和暗色模式不打架t3code的UI层是基于Tailwind CSS和shadcn/ui做的。shadcn/ui不是一个npm包它是一堆可以拷贝进你自己的项目的组件源码。好处就是组件没有锁死在某个版本里你想怎么改就怎么改。暗色模式这块t3code内置了next-themes方案切换逻辑很简单import { useTheme } from next-themes; function ThemeToggle() { const { resolvedTheme, setTheme } useTheme(); return ( button onClick{() setTheme(resolvedTheme light ? dark : light)} {resolvedTheme light ? 暗色 : 亮色} /button ); }t3code的CSS变量采用了HSL色彩系统方便在明暗两种主题下调整色相。如果品牌色是蓝色改一处--primary变量就可以全站生效不需要一个个组件去改。需要提个醒Tailwind CSS 4 和 Tailwind CSS 3 的配置方式差别很大t3code最开始是用的3.4版本。如果你是照着网上老教程自己配的建议直接看t3code仓库里最新的tailwind配置别混着两个版本来很容易出现配置了但不生效的情况。4. 构建部署与线上运维4.1 多环境变量管理dev、preview、production不串门工程化项目一个特别容易栽坑的地方就是环境变量。团队协作时每个人本地的DATABASE_URL、NEXTAUTH_SECRET都不一样如果代码里写死一提交就会把别人的配置带乱。t3code的规则是.env不提交到仓库.env.example提交里面写上所有变量名和假值新同事复制改名即可。部署时的三个环境对应三组配置基本规则如表所示环境数据库典型场景变量来源developmentSQLite / 本地PostgreSQL日常开发调试本地.envpreview临时PostgreSQL数据库PR预览、联调部署平台环境变量production托管PostgreSQL正式发布CI/CD密钥库我最常遇到的坑是NEXTAUTH_SECRET忘了在正式环境设置导致登录状态一刷新就失效。这个secret在NextAuth下是必须的可以用openssl rand -base64 32生成不要用简单字符串凑合。4.2 Docker化部署写一个不会在凌晨三点报警的Dockerfile部署方面t3code提供了一份标准的Dockerfile和docker-compose配置专门处理Next.js多阶段构建。Next.js需要生成Standalone输出才能减小镜像体积这一步必须在next.config.js里显式开启const nextConfig { output: standalone, // ... }; module.exports nextConfig;Dockerfile的核心结构我简化一下FROM node:20-alpine AS base FROM base AS deps WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci FROM base AS builder WORKDIR /app COPY --fromdeps /app/node_modules ./node_modules COPY . . RUN npx prisma generate RUN npm run build FROM base AS runner WORKDIR /app ENV NODE_ENVproduction COPY --frombuilder /app/.next/standalone ./ COPY --frombuilder /app/.next/static ./.next/static COPY --frombuilder /app/public ./public EXPOSE 3000 CMD [node, server.js]这里有个细节值得说明必须在构建阶段先执行prisma generate否则Prisma Client不会生成后面跑起来直接报找不到客户端。如果你用docker-compose还可以顺便把PostgreSQL容器一起编排本地一条命令起整个依赖栈联调体验非常接近线上。5. 常见问题排查与踩坑实录5.1 经典报错一PrismaClientInitializationError这个几乎每个用Prisma的人都会遇到。症状是运行时报“Prisma Client could not initialize”本质原因一般是prisma generate没有执行或者schema文件指向的数据库连接串不对。排查步骤很简单npx prisma generate npx prisma migrate dev如果你的项目是在Docker里跑的还要确认容器里有没有数据库网络权限DATABASE_URL里用localhost连接宿主机数据库是不行的要用docker-compose里的服务名。5.2 经典报错二Cannot use import statement outside a module这个多数出现在后端代码里混用了CommonJS和ESModule。Next.js默认支持ESModule但如果你引用了某个老旧npm包是CommonJS写法就会出现这类错误。t3code的处理方案是开启transpilePackages选项把需要转换的包列出来transpilePackages: [some-old-package],另外一个常见原因是某个包缺了main字段或者package.json里的type字段写错了。解决办法看报错堆栈定位到是哪个包再决定配置还是换一个替代。5.3 经典报错三Hydration failed because the initial HTML does not match client-rendered content这个报错很吓人提示你说服务端渲染的HTML和客户端渲染的对不上。t3code里我碰到过的原因是用了Date.now()或者Math.random()生成内容导致服务端和客户端产生了不一致。如果遇到优先排查有没有在渲染中直接使用这些非确定性API再加一个useEffect在客户端挂载后再生成数据。还有一个隐蔽的坑是用了localStorage做主题预判服务端渲染时读不到localStorage导致前后端渲染结果不一致。t3code里用next-themes封装了这块它默认禁止在服务端读取主题就是在规避这个经典问题。5.4 开发体验提升Turbopack和热更新配置Next.js 14支持用Turbopack跑开发服务器速度比Webpack快不少。t3code把dev脚本直接配成了dev: next dev --turbo实测下来大项目里保存代码后的热更新反馈时间基本压在200毫秒以内几乎察觉不到延迟。不过要注意Turbopack目前对个别插件的兼容性还不是百分百完美如果你遇到source map错乱或者某些webpack插件不生效可以临时切回普通模式对比一下。5.5 性能优化和SEO在模板阶段就做好的事我想补充一个观点性能优化不该在项目后期才做应该像t3code这样在模板阶段就把基础打好。内置了next/font自动字体优化图片全部用next/image做懒加载和尺寸优化Lighthouse的Performance得分在只有基础页面的情况下能稳定跑到95分以上。SEO方面用了Next.js内置的Metadata API每个路由文件里导出generateMetadata函数就能做到页面级别动态生成title和description不需要再额外引入react-helmet之类的库。RSC是App Router带来的新范式t3code的默认方式是能服务端取数的就服务端取数减少客户端请求次数首屏体验会明显舒服很多。结尾从我自己的体验说两句t3code从最初的个人脚手架迭代到现在我最大的体会是工程化不是炫技而是把隐形的复杂度用规则和工具收敛掉。当你不用每时每刻操心环境配置、类型飘忽、迁移脚本丢失这些琐碎事之后注意力才能真正集中在业务本身。最后分享一个扩展方向吧。t3code目前复用了现代全栈项目的标准骨架如果你想换掉其中的组件层比如改成纯CSS方案或者把数据库从Prisma换成Drizzle整体结构依然成立。你在尝试过程中如果遇到具体问题欢迎带着报错信息来跟我讨论我这边踩过的坑和验证过的方案还是蛮多的。