资讯动态

t3code全栈脚手架:从数据库到前端的一体化类型安全实践

发布时间:2026/10/17 3:43:58 来源:尧图企业网站定制
1. 项目概览与设计思路1.1 t3code是什么一个“类型安全优先”的全栈脚手架“t3code”这个名字我第一眼看到的时候脑子里就冒出来三个T开头的单词Type-safe类型安全、Testable可测试、Tidy整洁。到后面做得越深入越发现这三个T其实就是这套项目模板最核心的价值观。简单说t3code 是一套以 TypeScript 为底座、以 tRPC 为API层、以 Prisma 为数据层的现代全栈 Web 应用启动模板它解决了我在过往项目中反复遇到的一个问题前后端类型割裂、API文档滞后、数据模型一改就要到处同步开发体验极度割裂。这个模板解决的核心问题可以总结成一句话让类型从数据库一路贯通到浏览器端UI。以前我们写一个全栈应用后端定义好字段、数据库改好表结构、前端再手写对应的 interface三个地方要同步修改漏一个就出事。t3code 的做法是——数据库Schema作为唯一事实来源通过 Prisma 生成类型再通过 tRPC 让 API 层的输入输出自动推导前端调用时直接获得完整的类型提示字段改名、类型变动在编译期就能暴露问题。它适合谁用三类人一是想快速搭建内部工具或 SaaS 原型的全栈开发者不想在API层写大量重复路由代码二是团队里已经有 REST 风格 API 但深受类型维护之痛的工程负责人想尝试更现代的通信方式三是刚接触现代 TypeScript 全栈生态的同学想通过一个结构清晰、约定明确的模板快速建立完整的工程认知。我的建议是至少要有基础的 React 和 TypeScript 知识再上手会比较顺滑但这套模板本身会带着你理解每一层在做什么。1.2 技术选型分析为什么选这些而不是别的很多人问我为什么不用 Next.js API Routes axios 链路的传统方案答案在于tRPC 解决了多端类型复用问题而 REST 方案无法优雅地做到这一点。REST 当然不是不好但它的类型定义需要额外维护 OpenAPI 规范或者手写 types一旦项目进入中期接口数量过百之后维护成本会直线上升。GraphQL 也能解决类型复用的问题但引入了复杂的 Schema 语言和客户端缓存逻辑对于大多数中小型项目来说这个复杂度是不划算的。具体到 t3code 的组合我做选型时按层次拆解过Next.jsApp Router负责路由和服务端渲染生态系统成熟部署简单。TypeScriptstrict 模式全链路类型基础不严格就意味着白搭。tRPCAPI层的类型安全核心。它不发明新协议还是走 HTTP只是把输入输出的类型推断做完了。PrismaORM 和数据层类型来源Schema 即类型迁移工具链完整。Tailwind CSS原子化样式团队协作时减少 CSS 命名冲突和上下文切换成本。NextAuth.js认证集成方便接入各种 OAuth Provider专注业务逻辑而不是认证底层。这套组合其实很“激进”因为它把类型安全推进到了极致从前端的组件 props到 API 层的输入输出再到数据库记录类型全程没有手工编写任何 interface。这种体验一旦适应了就再也回不到手工维护类型的老路了。1.3 目录结构与模块边界t3code 的目录结构设计遵循了“按层分目录、按功能聚模块”的原则。看一个全栈项目我习惯先看目录结构因为结构本身就是架构设计的一部分t3code/ ├── app/ # Next.js App Router 路由层 │ ├── api/ # NextApi / App Router 路由少量边界情况使用 │ └── (routes)/ # 页面路由登录后可见的应用页面 ├── components/ # React 组件层 │ ├── ui/ # 基础 UI 组件 │ └── modals/ # 弹窗等复合组件 ├── server/ │ ├── api/ # tRPC root 路由与子路由定义 │ │ ├── routers/ # 业务子路由如 userRouter、postRouter │ │ └── root.ts # 根路由聚合 │ ├── auth.ts # NextAuth 配置与中间件 │ ├── db.ts # Prisma Client 实例化 │ └── api/trpc.ts # tRPC 上下文与初始化 ├── public/ # 静态资源 └── prisma/ ├── schema.prisma # 数据库 Schema 唯一事实来源 └── migrations/ # SQL 迁移文件关键点是业务逻辑不写在页面组件里而是集中在 server/api/routers 中页面组件只负责调用 tRPC 的 hooks 并渲染结果。这个约束让逻辑可以被单元测试覆盖也让页面代码从“业务夹生饭”变成了“纯UI描述”调试成本大幅下降。2. 核心细节解析与实操要点2.1 TypeScript 严格模式与路径别名的正确配置t3code 的 tsconfig.json 是整个类型体系的基石。我最看重的一项配置是strict: true以及noUncheckedIndexedAccess: true。后者很多人会忽略但它能逼你处理数组/对象索引访问时为 undefined 的情况这对消除运行时异常非常有价值。路径别名我用的是/*映射到根目录这样无论 import 写在哪一层都不需要搞一串../../../这种面条式路径{ compilerOptions: { strict: true, noUncheckedIndexedAccess: true, baseUrl: ., paths: { /*: [./*] } } }设置好路径别名后还需要同步更新 ESLint 的 import 规则否则 eslint-plugin-import 会识别不了。在.eslintrc.cjs中补充settings字段settings: { import/resolver: { typescript: { project: ./tsconfig.json } } }这套配置做完所有 import 语句中的相对路径都会自动转为/server/...这样的别名形式。实际体验中这个细节对代码可读性的提升是立竿见影的尤其是团队协作时其他人一眼就能看出模块归属。2.2 tRPC 路由设计的生命周期与中间件机制tRPC 是一种 RPC 风格的类型安全通信方案它和传统 REST 最直观的区别在 URL 设计上REST 关心“资源”和“动词”tRPC 只关心“过程调用”。我在 project 中定义路由时遵循一个简单规则能表达动作的都用 mutation纯读操作用 query。一个典型的 tRPC 子路由长这样import { createTRPCRouter, protectedProcedure, publicProcedure } from /server/api/trpc; import { z } from zod; export const postRouter createTRPCRouter({ list: publicProcedure .input(z.object({ limit: z.number().min(1).max(100).default(10) })) .query(async ({ ctx }) { return ctx.db.post.findMany({ take: 10, orderBy: { createdAt: desc }, }); }), create: protectedProcedure .input(z.object({ title: z.string().min(1).max(100), content: z.string() })) .mutation(async ({ ctx, input }) { return ctx.db.post.create({ data: { ...input, authorId: ctx.session.user.id }, }); }), });这里有几个细节。publicProcedure和protectedProcedure是两种不同的 Procedure 类型前者不需要登录即可访问后者会在调用时先执行ctx.session.user校验不通过则直接抛 UNAUTHORIZED 错误。尤其值得关注的是input中间层用 zod 做运行时校验。这一点至关重要——因为 TypeScript 的类型检查发生在编译期但数据从浏览器传到服务器之后到底长什么样运行时是无法靠“声明”来保证的。用 zod 在 API 入口做一次校验数据合法性和类型安全才真正闭环。在 trpc.ts 中通过createContext把 NextAuth 的 session 注入到每个请求的 context 中这样所有子路由都能通过ctx.session拿到当前用户信息而不需要挨个去数据库查import { initTRPC, TRPCError } from trpc/server; import { createTRPCContext } from ./context; export const createTRPCRouter t.router; export const publicProcedure t.procedure; const enforceUserIsLoggedIn t.middleware(({ ctx, next }) { if (!ctx.session?.user) { throw new TRPCError({ code: UNAUTHORIZED }); } return next({ ctx: { session: ctx.session } }); }); export const protectedProcedure t.procedure.use(enforceUserIsLoggedIn);提示中间件中返回next({ ctx: { ... } })可以覆盖 context后续 Procedure 拿到的 ctx 就是增强后的版本。我经常用这个机制注入当前用户、多租户 id、请求链路追踪 ID 等既减少了重复代码也让类型自动同步。2.3 Prisma Schema 设计与迁移策略Prisma 模型是 t3code 全链路类型的起点。这里有一个设计原则尽量把约束写在数据库层而不是依赖应用层校验。举个例子Post 表我除了在 zod 里限制 title 长度还会在 Prisma schema 中加上db.VarChar(100)让数据库本身拒绝超长数据。双保险的好处是绕过 API 直连数据库的脚本管理后台、数据修复任务也无法写入脏数据。model Post { id String id default(cuid()) title String db.VarChar(100) content String db.Text() published Boolean default(false) author User relation(fields: [authorId], references: [id]) authorId String createdAt DateTime default(now()) updatedAt DateTime updatedAt index([authorId]) }迁移策略上我强烈推荐prisma migrate dev 作为日常开发主流程。每一次 Schema 变更都会生成迁移 SQL并且标注文件名的日期时间回放和审计都很方便。团队协作时优先合并主分支的迁移文件如果冲突宁可删掉本地未提交的迁移文件重来也不要手工改 SQL——手工改迁移文件是后期数据事故的高发根源。2.4 Tailwind 配置与设计系统约定t3code 里 Tailwind 的意义不只是“快速写样式”更是提供了一套团队可共识的设计变量。在 tailwind.config.ts 中我扩展了颜色和间距语义import type { Config } from tailwindcss; export default { theme: { extend: { colors: { primary: { DEFAULT: #0f766e, hover: #115e59, }, surface: #f8fafc, }, spacing: { 18: 4.5rem, }, boxShadow: { card: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06), }, }, }, plugins: [], } satisfies Config;配置上的心得是不要一开始就把所有颜色都抽成变量只抽出重复出现的核心色和阴影其余先用 Tailwind 默认色板。等设计稿稳定后再逐步迁移到自定义 token。过早抽象设计系统往往造成的是“为了抽象而抽象”的负担。3. 实操过程与核心环节实现3.1 环境准备与本地初始化要跑通 t3code本地环境需要 Node.js 18推荐用 nvm 管理版本、pnpm 或 npm我用的 pnpm因为依赖安装速度和磁盘占用明显有优势、Docker用来本地跑数据库以及尽量保持 Git 版本在较新状态。我的初始化习惯是先用git clone把模板拉下来然后立刻删除原有 Git 历史避免后续业务代码和模板 commit 混在一起git clone repo-url my-project cd my-project rm -rf .git git init git add . git commit -m chore: init project from t3code template之后安装依赖并启动数据库pnpm install docker compose up -d postgrest3code 提供了一个docker-compose.yml模板其中就定义了本地 PostgreSQL 服务的端口、用户名、密码和初始化数据库名。实际执行前需要检查.env文件中的 DATABASE_URL 是否和 docker-compose 中一致否则会连接失败。3.2 环境变量配置与数据库初始化t3code 的.env.example文件里所有键都被配置好了我们只需要复制为.env并填入实际值。关键的几项有DATABASE_URLpostgresql://t3code:t3codelocalhost:5432/t3code?schemapublic NEXTAUTH_URLhttp://localhost:3000 NEXTAUTH_SECRET一个足够长的随机字符串 GITHUB_IDgithub oauth app 的 client id GITHUB_SECRETgithub oauth app 的 client secret我第一次配置时犯过一个低级错误NEXTAUTH_SECRET 直接复制了模板里的示例值结果部署到线上之后发现所有会话在重启后都失效。这个值的生成可以简单用openssl rand -base64 32完成。数据库初始化分为两步走到这里时才真正生成数据表结构npx prisma migrate dev --name init npx prisma generatemigrate dev会根据 prisma/schema.prisma 里的模型创建出 SQL 迁移文件并执行generate则会在 node_modules/.prisma/client 中生成类型完整的客户端。需要记住的是每当你修改 schema.prisma 后这两条命令都必须依次执行否则编码时永远拿不到最新的类型定义。3.3 启动开发服务器与类型链路验证执行pnpm run dev后Next.js 会在 3000 端口启动开发服务器。此时打开浏览器访问http://localhost:3000就能看到 t3code 的默认落地页。但页面能打开只说明工作了一半真正需要验证的是“类型链路是真通的”。我的验证方法是写一个最简单但完整的 tRPC 调用在 page.tsx 中直接调用一个 query把返回结果渲染出来。此时页面编辑器里在.data.title后输入.VS Code 的自动补全应该给出和 Prisma schema 完全一致的字段列表——这就是“数据库到前端类型贯通”的最直观证明import { api } from /trpc/react; export default function Home() { const { data } api.post.list.useQuery({ limit: 10 }); return ( main h1当前文章/h1 ul {data?.map((post) ( li key{post.id} {post.title} - {post.author.name} /li ))} /ul /main ); }如果这一步能跑通说明 TypeScript、Prisma、tRPC 三个层次的配置都是正确的。这一步是 t3code 整个开发链路的地基地基不稳后面的业务代码再炫也没有意义。3.4 实现一个最小业务闭环文章发布功能纸上谈兵没有意义用一个真实的业务闭环来演示 t3code 的完整开发流程更有说服力。以“发布文章”为例整个流程是这样的第一步在server/api/routers/post.ts中定义createmutation这一步上面已经给出了代码。第二步在页面里写一个表单组件用api.post.create.useMutation()发送请求。第三步编写成功后通过utils.post.list.invalidate()让列表自动刷新。这里最关键的是最后一步。tRPC 客户端会生成一个utils对象它相当于“缓存管理器”。当你调用invalidate时所有关联的 query 缓存都会被标记为失效并重新请求。这样前端无需手动刷新页面数据就会自动同步use client; import { api } from /trpc/react; import { useState } from react; export function PostForm() { const utils api.useUtils(); const [title, setTitle] useState(); const [content, setContent] useState(); const createPost api.post.create.useMutation({ onSuccess: () { setTitle(); setContent(); void utils.post.list.invalidate(); }, }); return ( form onSubmit{(e) { e.preventDefault(); createPost.mutate({ title, content }); }} input value{title} onChange{(e) setTitle(e.target.value)} / textarea value{content} onChange{(e) setContent(e.target.value)} / button typesubmit disabled{createPost.isPending} {createPost.isPending ? 发布中... : 发布} /button /form ); }这个闭环走完之后你会发现一个很有意思的点整个过程中你没有写过一行 API 路由代码没有写过 axios 请求没有定义过一次接口类型。而前后端之间传递的数据依然是严格类型安全的任何一处字段名错误都会在编译期直接报红。这就是 t3code 想给开发者的核心体验。3.5 生产构建与部署要点本地开发跑通后部署时有一个坑需要提前规避Prisma 在某些构建环境下会尝试下载二进制引擎导致失败。在 Dockerfile 或者构建脚本中最好显式指定引擎类型prisma generate prisma migrate deploy next build线上环境建议用migrate deploy而不是migrate dev。前者只会静默执行已经存在的迁移文件适合生产环境后者会在本地生成迁移并同步 Schema不适合在生产环境跑。如果是部署到 Vercel 这类 Serverless 平台特别容易遇到一个问题DATABASE_URL中的localhost会被应用服务器访问——但应用服务器上根本没有本地数据库。所以数据库连接串必须改成指向托管数据库服务如 Neon、Supabase、Railway的地址。这个坑我第一次部署时踩得很瓷实本地一切正常一上 Vercel 就 500排查了半天才发现是环境变量问题。4. 常见问题与排查技巧实录4.1 类型推断失效排查为什么我改了数据库但前端还是旧类型结果这个问题出现的频率排在第一位。大概率是 Prisma Client 没有重新生成。因为 tRPC 的类型是通过 Prisma 的返回值推断出来的Prisma 客户端还保存着旧版本的模型信息前端类型自然不更新。我自己的排查流程是确认 schema.prisma 改好了且保存。终端执行npx prisma generate看输出是否正常。若 IDE 里还显示旧类型重启 TypeScript 服务VS Code 里按CmdShiftP执行 “TypeScript: Restart TS Server”。还是不行就删掉.next和node_modules/.prisma后重新pnpm install prisma generate。大多数人走到第 3 步就能解决。记得这一步其实不是 TS 缓存问题而是 VS Code 的 TS 语言服务对文件监听有时不够灵敏。4.2 Prisma 报错 “P1001: Cant reach database server”这个报错是告诉你能连上数据库服务器但端口或者凭据不对。排查就三件事是否用 Docker 启动了 PostgreSQL执行docker compose ps确认状态。.env中的 DATABASE_URL 用户名密码端口和 docker-compose 中定义的是否完全一致包括特殊字符是否需要 URL 编码。数据库是否真的初始化过如果容器是新起的可能需要docker compose exec postgres psql -U t3code -c CREATE DATABASE t3code;手动创建。另外如果你访问的是不为 localhost或 127.0.0.1而是容器名例如db的地址本地连接时大多数情况下会失败——因为 Node.js 进程在你的宿主机上访问localhost才行。4.3 NextAuth 登录后 session 为空用 GitHub OAuth 登录时经常遇到跳转授权完成后回跳页面上session.user有一半时间是 undefined。排查顺序为NEXTAUTH_URL必须和浏览器地址栏的实际 URL 完全一致。本地开发时如果通过http://localhost:3000访问这个值就不能写成http://127.0.0.1:3000两者在 OAuth 回调签名校验中是严格区分域名的。NextAuth 官方文档特别提醒Next.js App Router 下必须在 auth 配置中显式设置secret否则开发环境可能不报错生产环境会在用户会话校验时随机失败。检查server/auth.ts中配置的 Provider 回调地址GitHub OAuth App 设置里的回调地址必须是${NEXTAUTH_URL}/api/auth/callback/github。4.4 构建失败部署后提示NEXT_PUBLIC_*变量缺失Next.js 对NEXT_PUBLIC_前缀的变量有硬性要求它们必须在构建时就被内联进代码所以运行时再注入就晚了。如果你把环境变量放在构建完成之后的运行环境比如 Vercel 的 “Runtime Environment Variables”它是不会生效的。这一类变量的值如果在构建时需要用到就得配置在 Vercel/其他平台的 “Build / Environment Variables” 中。如果确实有“同一个构建产物在不同环境下用不同接口地址”的需求我建议走纯服务端方案用 tRPC 的 server 端逻辑读取普通环境变量前端只通过 API 获取。4.5 常见问题速查表现象大概率原因解决动作类型不更新Prisma Client 未重新生成npx prisma generate并重启 TS ServerP1001 数据库连不上数据库没启动 / URL 配置错误检查 Docker 和 .env 的 URL登录后 session 为空NEXTAUTH_URL 不一致修正 URL 完全一致并配置 secret构建期变量缺失NEXT_PUBLIC_ 变量未在构建时注入配置 Build Environment VariablestRPC 返回 401忘记用 protectedProcedure业务代码中复制 protectedProcedure自引用字段类型卡死Prisma 自关联模型类型循环引用在 schema 中显式定义关系名称5. 进阶从个人模板到团队规范5.1 团队协作时的目录规约一个人用 t3code 跑起来容易团队一起用就会遇到边界模糊的问题。这里分享几条我们在实践中沉淀出来的规则组件层只允许使用components/ui下的基础组件和components/modals中的复合组件。任何业务域内的组件比如 PostCard都放到components/widgets或页面目录下防止业务组件被其他模块误用。路由层每个业务域的 tRPC 子路由对应server/api/routers下的一个文件。文件不要为了刻意拆小而拆小一个域一个路由文件方法太多才考虑按子域拆分。数据访问禁止在组件和页面中直接调用ctx.db或prisma.xxx。一切数据库访问必须通过 tRPC procedure。这样做的原因是统一入口让权限校验、日志、数据缓存都有地方放不会被绕过去。这套规约维护成本很低但对架构腐化的抵抗力非常强。团队里即使有新人只要遵循目录结构和代码分层约定都能写出风格一致、可维护的业务代码。5.2 中间件的进阶玩法日志、审计与多租户tRPC 中间件的能力被大多数人低估了。除了权限校验中间件还能实现请求日志、操作审计、多租户隔离甚至链路追踪。我们团队在多租户项目中用过一个租户解析中间件核心逻辑大概是这样const tenantMiddleware t.middleware(async ({ ctx, next }) { const tenantId await resolveTenantFromHost(ctx.headers.host); if (!tenantId) { throw new TRPCError({ code: FORBIDDEN }); } return next({ ctx: { tenantId } }); }); export const tenantProcedure protectedProcedure.use(tenantMiddleware);然后子路由里所有 query 都自动带上tenantId过滤list: tenantProcedure.query(async ({ ctx }) { return ctx.db.post.findMany({ where: { tenantId: ctx.tenantId }, }); });每个请求都会自动调用解析函数业务代码里再也不用担心“忘了过滤租户”这种数据泄露级别的低级错误。这是我在 t3code 里最喜欢的一个中间件用法强烈推荐你在自己的项目里试试。5.3 后续扩展方向建议t3code 这套模板的骨架是足够稳固的扩展时我的建议排序是先加监控与日志、再加CI/CD 流程、之后才是微前端或微服务。监控层面接入 Sentry 的 tRPC 中间件只需要几行代码却能在生产环境事故发生后迅速定位到具体是哪个 procedure 出的问题。CI/CD 层面最少也要做三件事TypeScript 类型检查、prisma migrate deploy前自动执行prisma generate、生产环境构建参数校验。这三件事我整理成了一个.github/workflows/ci.yml模板可以直接用。如果你更偏实验精神可以看看 t3code 后续和Server Actions的取舍Next.js 的 Server Actions 能直接在服务端执行函数不需要 tRPC 层但缺失了 tRPC 的“中间件统一错误处理缓存管理”这套机制。我个人的经验是页面内简单操作可以用 Server Actions但凡是会被多个复用组件调用的数据逻辑都走 tRPC 更稳妥。6. 踩坑后的几点真话文章最后不喊口号就说点实际操作后的体感。t3code 最让我上头的不是它省了多少模板代码而是它吞掉了“前后端类型不一致”这类问题的出现空间。以前做项目周五下午最怕的就是上线前突然发现接口字段对不上前端改了而后端没跟上或者是反过来。用了这套类型链路之后这类事故出现的概率趋近于零——因为编译阶段就挡住了。第二个强烈体感是t3code 对团队协作的隐形约束能力很强。它通过目录和 Procedure 的类型签名把“数据从哪里来、谁能调谁”固化成了代码层的约束。新人即使看不懂架构文档看到类型报错也会本能地遵循约束。这一点比任何 Code Review 清单都更有效。最后分享一个小技巧如果你经常初始化项目不妨把 t3code 压缩包放到 Git 仓库的templates分支里然后写一个create-project.sh脚本自动 clone、删历史、装依赖、启动数据库。这个过程全跑通只需要两三分钟非常舒服。根据我的经验一次成功的脚手架的标志是三个月后你再看它依然能迅速定位到某一层在做什么——t3code 做到了这一点希望你也能。

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

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

免费获取报价 →
↑