首页 / 资讯中心 / 文章详情

TypeScript全栈脚手架t3code实战:从零搭建到部署

TypeScript全栈脚手架t3code实战:从零搭建到部署 ★ FEATURED ARTICLE
这几年写全栈项目我试过不少脚手架最后留在手上的一个是t3code。它不是某个大厂的框架而是一套以TypeScript为底、把Next.js、Tailwind CSS、tRPC和Prisma组装起来的全栈模板。用t3code起新项目数据库表结构、服务端接口、前端页面甚至类型定义能一次串通不用再花大半天时间去拼胶水代码。今天这篇东西就是把我从零搭项目、改模板、部署上线的完整过程记下来给那些准备用全栈TS技术栈但又不想从裸Next.js一点一点磨的朋友做个参考。1. t3code到底解决了什么问题先聊清楚一件事t3code不是某个必须安装的运行时也不是一套封闭的框架。它本质上是把当下TS全栈开发里最常用、配合最顺手的几个工具提前按最佳实践组织好。你拿到的是一套能跑的工程骨架里面路由、数据库连接、认证逻辑、样式方案和API层之间已经彼此打通。平时写项目最耗时间的就是这些基础模块之间的对接t3code把这一层做掉了。我最早接触这类模板的动机很简单。当时接了一个内部工具类的项目业务其实不复杂但被几件事拖住了进度一是要维护两套类型定义后端改了字段前端不知道二是数据库迁移脚本和业务代码混在一起每次改表结构都心惊胆战三是服务端接口文档写不写都难受写了没人看不写又容易出错。后来把技术栈换成这套组合上面三个痛点基本消失。为什么这么有效核心在于TypeScript的端到端类型安全。后端写好的查询结果是什么类型前端调用时自动推断出来字段改名之后编译阶段就能发现所有引用点不用靠人肉搜索。这种体验在中小型项目上特别重要因为你往往没有多余的人力去维护一份API文档更不会有人愿意写两遍类型定义。另外一点t3code对新手和老手都有价值。新手拿它当学习模板可以看出现代全栈TS项目是怎么把路由、数据库和UI组织起来的老手拿它当起点省去重复搭建工程的时间直接往里面填业务代码。我在实际使用中感受到的最大变化是项目启动速度明显加快。以前新项目光是环境配置、目录规划、模块间通信方式就得折腾一天现在基本半小时内进入业务开发状态。说到底t3code的核心价值不是某个单一技术有多强而是把多个技术正确组织在一起之后产生的那种顺畅感。它更像一套被验证过的组合拳你只需要关心业务本身。2. 技术栈选型与架构拆解2.1 为什么是这套组合方式t3code背后的技术组合通常被称为T3 Stack。它不追求大而全而是选了四条主线TypeScript提供类型基础Next.js承担页面路由与服务端渲染Tailwind CSS处理样式tRPC串联前后端接口调用。如果再算上数据库层Prisma负责Schema定义与迁移。这四个工具放在一起工作流非常自然。以我常做的后台管理系统为例数据模型用Prisma的Schema定义一旦定义完成类型会同步生成到前端代码里。前端组件里写一个查询调用总线层自动把参数和返回值类型都约束好根本不需要额外定义接口类型。以前项目里那种传错字段名导致线上才出错的情况现在编译阶段直接挡掉。我特别想聊一下tRPC。很多人第一次看到这个词会愣一下觉得和REST、GraphQL比有点陌生。实际使用下来它解决的是一个非常具体的问题在前后端都使用TypeScript时为什么还要手动定义一套接口描述tRPC允许你像调用本地函数一样调用服务端方法类型直接从服务端推导到客户端。这不是什么魔法只是在构建期用TypeScript的类型推断做了一次跨越网络边界的连接。那什么时候不适合tRPC这种模式如果你要面向第三方开放API或者前后端并非同一套TypeScript代码库那tRPC的优势就弱了更通用的REST或GraphQL会是更好的选择。而在内部项目、全栈型应用或团队本身就是TS技术栈的情况下tRPC的开发效率优势非常明显。2.2 核心目录结构与职责划分t3code生成的项目目录不是随便排的每个目录对应一个明确职责。看得懂这套结构后面加业务模块会非常顺手。我用一个实际项目里的目录来说明prisma/ schema.prisma src/ components/ pages/ server/ api/ auth/ db/ trpc/ styles/ utils/pages目录放路由页面这是Next.js的约定。server目录比较关键它把服务端逻辑集中在一起避免散落在各处。其中trpc目录定义路由db目录统一管理数据库客户端实例auth目录负责认证逻辑。components目录放可复用的React组件utils目录放纯函数工具。刚开始用这套结构的人最常犯的错是把业务逻辑直接写在page组件里。页面一旦复杂起来组件变得又长又乱查询逻辑和渲染逻辑缠在一起。正确做法是页面组件只负责取数和渲染业务规则尽量下沉到server层通过tRPC路由暴露给前端。这样后期维护时会轻松很多。另外prisma/schema.prisma是整个数据结构的唯一事实来源。你不需要手写SQL建表也不需要维护独立的迁移脚本集合。每次改动Schema之后跑一次迁移命令数据库表结构会跟着更新。这个设计直接消除了“代码里的模型”和“数据库里的表”两套系统不一致的问题。2.3 类型安全在日常开发里的价值体现类型安全不是嘴上说说的优点它会在日常开发中不断替你挡坑。举一个真实例子业务里有一个订单状态字段取值范围是pending、paid、shipped、completed。在传统REST接口模式下前端调用方拿到的是一个普通字符串你只能在运行时去校验它是否合法。在t3code这套体系里后端函数返回类型就是一个联合类型前端代码里写错了枚举值编译直接暴露问题。这种约束看起来很小连续做三个项目之后你会明显感觉到“写错字段”这类低级错误的频率降到了接近零。尤其在多人协作时新同事接手一个模块不需要反复问后端这个字段是什么意思、那个参数是可选的还是必填的代码提示会告诉他所有信息。类型安全的另一个隐性好处是重构时心里有底。业务迭代速度快的时候改一个数据结构是常有的事。以前改完字段还要全局搜索所有调用点检查有没有遗漏。现在只要改Prisma Schema和相应的tRPC路由前端所有引用点的类型校验会告诉你到底哪些地方受影响。几个Grep命令都省了。3. 从零搭建一个基于t3code的实际项目3.1 环境准备与项目初始化写这部分之前先说明一下我下面用的命令和配置基于近期常用版本。这类模板更新比较快具体输出可能有点出入但整体流程是稳定的。准备开始之前你需要确认本机已经把Node.js装好。初始化命令很简单npm create t3-applatest my-project执行之后安装向导会问你要不要启用NextAuth、Prisma、Tailwind、tRPC这些模块。第一次使用建议全选后面再按需裁剪也行。它还会问你使用npm还是pnpm管理依赖我的建议是pnpm磁盘占用和安装速度都更理想尤其是在一个机器上开多个项目的情况。初始化结束之后看到的标准输出里会有一串提示教你启动开发服务器cd my-project npm run dev这时候浏览器打开localhost:3000会看到一个默认首页。很多初次使用的人到这里就以为模板已经可以跑了但实际上数据库还没接上。下一步要做的是配置数据库连接串和准备环境变量。3.2 数据库Schema设计与迁移我用一个笔记类应用来演示。业务不算复杂两张表就够了用户表存放账号信息笔记表存放内容、所属用户和创建时间。在prisma/schema.prisma里定义如下model User { id String id default(cuid()) email String unique name String? notes Note[] createdAt DateTime default(now()) updatedAt DateTime updatedAt } model Note { id String id default(cuid()) title String content String owner User relation(fields: [ownerId], references: [id]) ownerId String createdAt DateTime default(now()) updatedAt DateTime updatedAt }定义好Schema之后执行数据库迁移命令npx prisma migrate dev --name init这个命令会做三件事比较Schema与当前数据库状态、生成迁移SQL、同步最新结构。执行完成后会在prisma目录里生成迁移文件同时更新TypeScript客户端类型。这里有一个新手容易忽略的小细节cuid和自增id的选择。很多从传统后端转过来的习惯用自增整数但在Prisma和tRPC这套体系里cuid字符串更省心。原因很简单自增id容易暴露数据规模而且在分布式或数据合并场景下容易撞车。cuid虽然看着长一点但省掉一堆潜在问题。3.3 用tRPC写第一个后端接口数据库就绪之后下一步是写接口。在t3code模板里定义一个tRPC路由非常简单。我在server/api/routers/note.ts里新建了笔记路由import { z } from zod; import { createTRPCRouter, protectedProcedure } from ~/server/api/trpc; export const noteRouter createTRPCRouter({ list: protectedProcedure.query(async ({ ctx }) { return ctx.db.note.findMany({ where: { ownerId: ctx.session.user.id }, orderBy: { createdAt: desc }, }); }), create: protectedProcedure .input(z.object({ title: z.string().min(1), content: z.string(), })) .mutation(async ({ ctx, input }) { return ctx.db.note.create({ data: { title: input.title, content: input.content, ownerId: ctx.session.user.id, }, }); }), });如果你之前只写过REST接口这段代码读起来会有种奇怪的感觉接口不再描述URL和HTTP方法而是直接变成函数。list是一个查询create是一个变更。过程代码里没出现任何手写SQL或ORM链式调用的模板噪声数据库操作和类型约束在同一段代码里完成。protectedProcedure的含义也解释一下这个路由受到认证保护调用前必须带上合法会话。会话里的用户信息通过ctx.session.user.id直接取到不需要在业务代码里再手动做权限判断。当业务里有更多路由需要同样的权限控制时只要换成protectedProcedure就行不用重复写校验逻辑。写完路由记得在根路由文件里注册。t3code里通常有一个合并根路由的地方类似export const appRouter createTRPCRouter({ note: noteRouter, });不注册的话前端是调不到这个路由的。这个环节漏掉之后报错还不太明显容易排查半天。3.4 前端页面接入与数据展示接口就绪前端调用非常直接。在Next.js页面组件里用模板生成的API辅助函数调用后端import { api } from ~/utils/api; export default function NoteListPage() { const notes api.note.list.useQuery(); if (notes.isLoading) return div加载中.../div; if (notes.isError) return div加载失败/div; return ( ul {notes.data.map((note) ( li key{note.id} h3{note.title}/h3 p{note.content}/p /li ))} /ul ); }注意useQuery这里的类型推断。notes.data不需要手动声明类型它是noteRouter.list返回值的自动推导结果。页面里的字段名如果写错IDE会直接标红。这比我在老项目里依赖后端swagger文档再手写接口类型效率差的不是一点半点。提交新数据时用mutationconst createNote api.note.create.useMutation({ onSuccess: () { // 触发列表重新拉取 notes.refetch(); }, }); createNote.mutate({ title: 测试, content: 内容 });这个模式我特别喜欢的一点是状态管理根本不需要额外引入。useQuery自带loading、error、data状态useMutation可以监听成功失败回调。业务复杂程度不高的情况下Redux这类全局状态库完全不需要进技术栈。4. 认证、安全与权限配置4.1 基于NextAuth的完整接入流程大部分真实项目都绕不开登录。t3code模板里集成了NextAuth接入时你要做的事比想象中少。先安装认证相关依赖然后在环境变量里配置会话密钥# .env NEXTAUTH_SECRET一个足够长的随机字符串 NEXTAUTH_URLhttp://localhost:3000NEXTAUTH_SECRET是会话签名的密钥重要性很高。开发环境随便写一个没问题生产环境千万不能用弱字符串。一个简单生成方法openssl rand -base64 32然后配置认证提供商。以邮箱加密码登录为例需要在server/auth.ts里配置对应的Provider。NextAuth支持很多ProviderGitHub、Google这种第三方的最省事配置好Client ID和Secret就能用。但如果是企业内部系统邮箱密码登录反而更可控。认证接入完成后你的服务端路由可以分两种公开的和受保护的。前面的note路由用的protectedProcedure就是受保护过程。未登录用户访问这些接口时直接被tRPC中间件拦下并返回未授权错误。这个机制比起在页面里手动判断登录状态要稳妥得多因为安全校验发生在服务端前端根本绕不过去。4.2 权限边界与数据隔离的常见陷阱很多人以为登录做好了权限问题就解决了。实际上登录只是认证权限是另一回事。以笔记应用为例每个用户只能读自己的笔记这个约束必须同时存在两个层面。第一层是前端显示只展示当前用户的数据第二层是后端查询查询条件强制带上ownerId等于当前会话用户id。如果只做第一层一个用户改一下请求参数就可能看到别人的数据。在t3code里第二层约束写在后端路由里任何前端调用都必须经过它这就杜绝了直接越权的可能。实际项目里遇到最常见的问题是开发时图方便把某些查询写成了查询所有记录。上线前安全测试很容易扫出这类接口。我的习惯是所有列表类查询默认带用户维度过滤条件除非这个表本身就是全局公开数据。这个习惯是从一次线上数据泄露的演练里总结出来的那次还好只是测试数据但足够让人后背发凉。还有一个小细节是敏感字段的返回控制。比如用户表里有密码哈希绝对不要直接整表返回。Prisma支持字段级别的select控制在tRPC路由里查询时只select需要的字段避免把无关数据暴露给前端。4.3 会话管理和过期处理NextAuth的会话管理默认走JWT或数据库会话两种方式。模板默认用的是JWT好处是无状态服务端不需要额外存会话记录。代价是会话失效之前无法主动踢人。如果你的系统有强制下线、封号之类的需求建议改用数据库会话模式。开发环境里会话过期时间默认较长方便调试。但在生产环境我通常会把会话有效期调短一些配合刷新机制使用降低会话被劫持后的风险窗口。这个参数在auth配置里可以显式设置session: { strategy: jwt, maxAge: 60 * 60 * 24 * 7, // 7天 }这里具体怎么配取决于系统对安全性和易用性的平衡。金融类系统可能需要短会话加双因子认证内部工具则更注重用户免打扰。没有绝对正确只有适合场景。5. 部署与性能优化5.1 前后端一体化部署方案t3code生成的是一个完整的Next.js应用部署时不需要分开部署前端和服务端。最常见的方案是把整个项目部署到支持Node.js的Serverless平台或直接部署到一台VPS上用Docker跑。我的大多数项目选的是平台托管配置简单不容易出错。部署前需要确认几个环境变量都配置好了数据库连接串、NEXTAUTH_SECRET、NEXTAUTH_URL。环境变量在本地和线上必须分开配我见过有人把生产数据库地址复制到本地配置里差点把重要数据改掉的情况。本地开发永远用本地数据库线上配置只在部署平台环境变量里存在。构建命令通常就是npm run build模板自带优化配置。构建成功之后平台会自动处理好静态资源和服务端逻辑的分配。接下来只需要把数据库迁移脚本在线上执行一次整个应用就能跑起来了。5.2 常见构建配置与调优项如果不满足于默认配置有几个调优点值得关注。第一Next.js的静态生成策略。对于内容不常变的页面可以开启静态生成让页面在构建时就生成HTML而不是每次请求时再渲染。t3code模板中很多页面是服务端渲染如果你的页面数据基本不变改成本地静态生成可以显著降低响应延迟。第二tRPC调用本身走HTTP网络开销不是零。如果组件里有多个并行查询可以考虑用Promise.all合并成一个请求。第三数据库查询性能。Prisma生成的查询性能在大多数场景下没问题但如果数据量大还是要看慢查询日志。比如笔记列表接口虽然只查当前用户的数据但如果没有在ownerId字段上建索引全表扫描会非常吃力。这种简单索引在Schema里用index定义迁移一次即可。5.3 监控与上线后的持续保障部署上线只是开始。我的项目里会加一个简单的健康检查接口返回数据库连接状态和基础环境信息。不管是用平台自带的监控还是外部探活服务这个接口能帮你第一时间发现服务不可用的问题。日志也很重要。Serverless平台通常自带日志聚合本地开发时一定要养成看控制台日志的习惯。很多问题在开发阶段其实已经出现了只是被你的操作路径掩盖住。比如某条数据导致查询报错你在页面上点击的流程刚好绕开了它但线上用户的操作路径不可控可能就触发那个分支。数据库层面每日自动备份是底线。即便只是个小项目备份开销也不高但真出问题时能救命。我吃过一次亏数据库没有备份配置一次误操作导致数据恢复不回来。从那以后不管项目大小备份脚本永远先配置好。6. 常见问题与排查技巧实录6.1 环境变量与数据库连接类问题这类问题出现的频率最高症状往往是接口报错或页面白屏。最常见的原因有三个环境变量没写对、数据库没起来、数据库连接串格式不对。环境变量没写对典型的现象是运行时提示缺少某个配置项。排查方法是先去.env文件确认键名是否和模板里一致。注意不要在.env文件后加多余空格或分号这类小错误特别坑人看起来毫无问题但解析失败。数据库连接不上时会看到类似connection refused的报错。先检查数据库服务是否在运行再检查连接串里的主机、端口、用户名、密码是否准确。如果用的是远程数据库还需要确认网络是否可达。不要跳过ping这一步很多连接失败都是网络原因。还有一个容易忽略的点Prisma客户端在构建时读取环境变量。如果你改了.env文件但构建进程没有重新读取配置旧配置可能还在生效。遇到改了连接串仍然连旧库的情况重启一下服务或重新构建通常能解决。6.2 类型报错与Schema不同步使用t3code时前端页面提示某个字段不存在或者类型不匹配。几乎都是因为改了prisma schema之后没有重新生成Prisma Client。记得每次修改Schema并执行迁移之后重新生成客户端类型npx prisma generate这个过程会把最新的表结构同步到TypeScript类型里。如果跳过这步代码还在使用旧的客户端类型自然报错。多人生涯协作时还会遇到另一个情况同事改完Schema并推送代码你pull下来之后本地没有执行迁移数据库表和代码模型不一致。标准操作是pull代码后跑一遍迁移命令让本地数据库同步。如果本地已经有了自己实验产生的脏数据迁移可能会失败这时候只能谨慎处理冲突必要时重置本地开发库。6.3 路由权限与实际场景不匹配这个问题的表现是页面看起来正常但接口调用返回401未授权。排查时先确认前端是否真的带着会话请求接口。NextAuth的会话在浏览器里以Cookie形式保存前端调用tRPC时默认不会自动附带Cookie需要在tRPC客户端配置里把Cookie携带逻辑接好。模板本身已经处理了但如果你改了自定义请求逻辑容易把这层弄丢。还有一种情况是接口本来设计为公开但误用protectedProcedure导致匿名用户无法调用。如果要开发匿名可访问但登录用户有额外数据的能力一般做法是写两个路由或用可选会话的中间件而不是让匿名用户走受保护流程。6.4 性能瓶颈和渲染策略问题页面加载慢不一定都是后端慢。有时是前端组件在客户端发起请求导致首屏渲染等待时间较长。优化方式有两种一是在服务端组件里直接取数渲染时把数据带到页面里二是把不重要的区域改为懒加载用户滚动到那块内容时再触发查询。另外tRPC请求的缓存策略也值得关注。默认情况下useQuery的数据会被缓存一段时间避免重复请求。如果你发现数据迟迟不刷新那可能是缓存策略导致的。需要手动失效缓存或降低缓存时间。这个和React Query的缓存机制一致熟悉React Query的很快能上手。很多人在使用过程中觉得复杂其实是从REST思维切换到函数调用思维的不适应。等适应了这套模式你会发现需要手动处理的东西反而更少。建议在第一个项目里尽量按模板约定走不要一上来就做自定义改造。先感受默认行为的合理性再根据实际需求去调整。7. 我对t3code这套工作流的最终心得用了不短的时间我最大的感受是t3code不是帮你自动写代码的魔法而是把那些本该约定好的事情提前约定好。当你不需要纠结路由API怎么写、类型定义放哪里、数据库客户端怎么初始化时注意力才能真正集中在业务逻辑上。这个转变非常微妙但它确实改变了我的日常开发节奏。按照模板约定理解它为什么这么设计往往比绕过限制更重要。很多看似多余的约定拆掉之前觉得省事拆完才知道那层约束帮了多少忙。类型安全这件事尤其如此它偶尔会让你在写代码时多思考几秒但那些被挡住的错误原本可能要等到线上用户遇到才会暴露。最后分享一个实用的小技巧。我在多个项目里复用t3code模板时会把一些常用的业务模块做成独立路由文件存起来。下次新项目需要类似功能直接复制路由文件再稍改一下数据模型就能用。虽然t3code本身是新项目的起点但你自己的经验积累才是模板之外最值钱的部分。希望这篇记录能给你提供一些有用的参考。
阅读完成 · 觉得有帮助?
咨询建站