ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

T3 Stack 全栈项目实战:基于 t3code 打造完整 Web 应用

T3 Stack 全栈项目实战:基于 t3code 打造完整 Web 应用

如果你在搜索引擎里敲下t3code,多半是在找一个能直接抄作业的全栈项目,而不是想看"Next.js 很强大"这种正确的废话。我前阵子正好把一个基于 T3 Stack 的实战项目整理成了这套t3code,包含用户注册登录、文章发布、评论互动、点赞关注、后台管理这些完整业务闭环。考虑到仓库名只有这么几个字,我按自己团队里跑得最稳的一套 T3 Stack 实现来展开讲述,代码细节会尽量给到位。

t3code是一套使用 T3 Stack 构建的全栈应用示例代码。T3 Stack 这个名字可能有人听过,它是目前 TypeScript 全栈生态里很流行的一套组合拳:Next.js 负责页面和渲染,TypeScript 负责类型检查,tRPC 负责前后端接口调用,Tailwind CSS 负责样式,Prisma 负责数据库操作,NextAuth.js 负责登录认证。这套技术选型并不是为了追新,而是为了让开发者在写业务的时候少操一份心。t3code存在的意义就是帮你把这几样工具串成一个能跑、能开会、能上线的产品,同时把过程里容易踩的坑提前标出来。

这个项目尤其适合三类人。第一类是刚学完 React、正准备跨进全栈的新手,跟着代码走一遍能少走很多弯路。第二类是技术负责人,想拿一套内部培训用的参考代码,t3code的目录结构和服务端划分可以作为团队规范的模板。第三类是独立开发者,周末想验证一个产品想法,用这套代码十分钟就能起一个带数据库和登录的 MVP。如果你属于其中任何一类,下面这套拆解应该能给你省下不少时间。

我现在不会按"先讲 Next.js、再讲 Prisma、最后讲部署"这种孤立的方式来说,因为你看完还是会懵。我按真实的项目生长顺序来:先讲整体设计为什么要这样拆,再讲核心细节怎么落地,然后亲手带着你把关键模块实现一遍,最后把常见的坑和排查方法列出来。这样无论你是想直接拿来改,还是想理解原理,都能有收获。

1. 内容整体设计与思路拆解

1.1 把全栈应用的复杂度压到最低

真正动手写过全栈项目的人都知道,最折磨人的不是某个单独的技术难,而是"连起来"的过程。前后端怎么约定接口?字段校验要写几份?后端改了一个字段类型,前端会不会第二天才发现?部署到线上之后数据库连接串怎么管理?这些问题如果只有一个框架,它们是分散的;但当你用一整条链路去解决,体验会完全不同。

t3code的设计初衷,就是把这类"连接成本"压到最低。我选了 T3 Stack 作为整个代码库的底座,打通了六个环节:Next.js 处理页面与服务端渲染,TypeScript 保证类型安全,tRPC 负责前后端调用,Tailwind 快速出样式,Prisma 管理数据库,NextAuth 处理登录。这个组合不是随便拼的,它们有一个共同点:都把开发体验放在优先级很高的位置。尤其是 tRPC,后端定义一个查询,前端立刻拿到完整类型提示,接口文档在这一套体系里几乎可以消失。

在动手写之前,我先反复问了自己一个问题:t3code到底要做一个示例,还是一个能作为产品起点、长期维护的模板?最终我选择了后者。因为如果只是一个 Demo,我会怎么写开心怎么来,但那对你没有参考价值;如果把它当成产品起点,每一条代码都要考虑"三个月后还能不能看懂"。事实证明,这个选择几乎影响了后面的所有细节。

另外一个关键选择是"约定优于配置"。t3code没有发明新架构,而是老老实实遵循 create-t3-app 的目录约定:src/server放所有服务端逻辑,src/app放页面,src/trpc放 tRPC 的初始化代码。这样任何一个熟悉 T3 Stack 的人接手,都能第一时间找到对应文件。项目代码的可读性有时候比某个巧妙的设计重要得多,尤其是当你不打算一个人维护的时候。

1.2 看一遍六层依赖与一次完整请求

t3code里的层次关系很清晰,从底到上可以画成一张表。不需要背,写代码的时候我会顺手对照:

层次位置负责的事
数据库PostgreSQL / SQLite持久化存储全部业务数据
Prisma 数据层src/server/db.ts提供唯一数据库客户端实例
tRPC 服务端src/server/api/root.ts与 routers定义全部业务 API
tRPC 客户端src/trpc/react.ts前端调用 API 的统一入口
Next.js 页面src/app服务端渲染与客户端交互
样式层Tailwind CSS设计系统与原子类

以t3code里最核心的"文章发布"功能为例,一次普通的读请求长这样:浏览器请求 Next.js 页面,Next.js 在服务端调用 tRPC 的post.list过程,tRPC 通过 Prisma 查询数据库,拿到数据后返回给 Next.js,由服务器组件渲染成 HTML。整个过程只有一个 API 层,不存在 REST 路由、redux、axios 这些中间人。

这个设计的精妙之处在于,t3code把数据获取的入口统一收敛到了 tRPC。前端组件里可以直接写const posts = await api.post.list.query(),然后放心使用返回结果的类型。如果数据库表结构改了,Prisma 会重新生成类型,TypeScript 立刻高亮所有受影响调用点,一个错误的修改根本走不到运行环节。

关于"类型安全链路",我用生活化的方式解释一下:以前你前后端两个人各写一份接口文档,后端改了字段你不一定知道,上线当天才发现参数名对不上,就像两个厨子各按自己的菜谱做同一道菜,出锅了才发现口味不一致。现在t3code的 TypeScript 类型从数据库一路穿透到页面组件,前后端手里拿的是同一份菜谱,改菜谱的地方必然连带改动所有依赖这道菜的地方,想不一致都难。

2. 核心细节解析与实操要点

2.1 目录结构里的设计语言

先看一眼t3code的核心目录结构:

t3code/ ├── prisma/ │ └── schema.prisma ├── src/ │ ├── app/ │ │ ├── (auth)/ │ │ ├── dashboard/ │ │ ├── layout.tsx │ │ ├── page.tsx │ ├── server/ │ │ ├── api/ │ │ │ ├── root.ts │ │ │ └── routers/ │ │ ├── auth.ts │ │ └── db.ts │ ├── trpc/ │ │ ├── react.ts │ │ ├── server.ts │ │ └── init.ts │ ├── components/ │ └── styles/ └── .env.example

prisma/目录里只有schema.prisma,所有数据模型定义都在这一个文件里完成,它是"数据库结构的唯一事实来源"。src/server/里又细分了api/、auth.ts、db.ts,分别放 tRPC 路由、NextAuth 配置和 Prisma 客户端实例。为什么要单独拎一个db.ts?因为 Prisma Client 应当作为单例存在。如果每个路由文件都new PrismaClient(),开发模式下热更新会导致连接数暴涨,终端里全是绿色的"Too many connections"警告,这是非常典型的血泪教训。

src/trpc/是整个代码库里最重要的一个目录。里面不仅有initTRPC、createTRPCContext,还定义了publicProcedure、protectedProcedure两个过程工厂。前者允许匿名访问,后者会先校验 session、再执行后续逻辑。t3code在做创建文章、删除评论这类写操作时,全部走 protectedProcedure,这样业务代码不用到处写权限判断。

前端部分没有把组件按"页面"硬切,而是按"功能区块"拆。auth/放登录注册相关组件,posts/放文章列表和编辑器。这一点我吃过亏:以前把所有组件堆进一个_components文件夹,项目一改版根本分不清组件属于哪条业务线。后来改成按 feature 组织,每次需求变更只需要打开对应目录,改代码范围一目了然。

2.2 为什么是 tRPC,而不是 REST 或 GraphQL

这是t3code在技术选型时争议最大的点。很多朋友一上来就问我:你为什么不老老实实写 REST 接口?我也不是没犹豫过。REST 直观,GraphQL 灵活,但 tRPC 的核心优势是"把 API 调用变成一次类型安全的函数调用"。在 Next.js 服务端组件里,你不需要 fetch URL、不需要解析 JSON、不需要记忆接口路径,直接调用导入进来的函数就行。写业务代码时你根本不需要关心"这个 API 的 URL 是什么",而只关心"我要拿到什么数据",开发速度完全是另一回事。

从网络层面讲,tRPC 只是把 JSON 数据包通过 HTTP POST 发到/api/trpc,本质上没有引入任何黑魔法。它和 REST 的差别在约定层面:REST 强调资源语义和 HTTP 动词,tRPC 强调调用过程和过程名。对于内部全栈项目来说,REST 的语义设计成本是不必要的开销;GraphQL 又复杂到需要团队统一规范才能玩得转。这不是说它们不好,如果你要开放 API 给第三方,我仍然建议用 REST 或 GraphQL,但t3code的定位是"快速构建内部产品",tRPC 刚刚好。

简单对比一下三者:

维度RESTGraphQLtRPC
类型安全需要手动维护类型和 schema通过 Codegen 间接获得原生类型推断
学习成本低高(schema 设计、缓存方案复杂)中低
适合场景开放 API、第三方集成复杂聚合查询、多客户端内部全栈产品
接口文档需要 Swagger 等需要维护 schema类型自带
调试手段浏览器直接访问Playground浏览器 Network 面板

如果你想体验这套类型穿透的爽感,可以做一个高频操作:在src/server/api/routers下新建一个 router,注册到根路由后,刷新一下页面,前端输入框里立刻能拿到新 router 下的所有 query。这个体验在新手阶段的冲击力很大,我第一次用的时候觉得不现实,以为是编辑器插件开的什么神秘通道,后来才明白这就是 TypeScript 推断的威力。

2.3 几个必须提前约定好的细节

  • 环境变量集中管理。t3code把DATABASE_URL、NEXTAUTH_SECRET、NEXTAUTH_URL都放在.env里,并用 Zod 在启动时校验。漏配一个变量,项目会在启动阶段直接报错,而不是运行后静默失败。这个体验对部署尤其重要,早一分钟失败,就少一小时排查线上诡异问题。
  • 前端组件不能直接 importserver/下的模块。Next.js 会报错,因为这会把 Node 服务端代码打进浏览器包。t3code在服务端入口文件顶部用了server-only包,误引用时编译阶段就会失败。
  • TypeScript 严格模式不要关。strict和 ESLint 规则都开着,代价是初期写代码时频繁被挑战,收益是之后每次改动代码都有安全感。喊严格模式没用的人,通常都是没在大型重构里受益过。

这些细节看起来琐碎,却决定了你能否在一个月后、三个月后继续维护这套代码。t3code里所有目录依赖方向都是朝内的:页面依赖 router,router 依赖服务端工具,服务端工具依赖数据库。任何依赖关系反向的结构,都会在 code review 里被当场打回。

3. 实操过程与核心环节实现

3.1 初始化项目与基础配置

上手t3code最快的路径,是从 create-t3-app 生成。我在本地执行:

npx create-t3-app@latest t3code

交互式提示一路问下来:要不要 TypeScript?要不要 App Router?要不要 tRPC?要不要 Prisma?要不要 Tailwind?要不要 NextAuth?这个脚手架的好处是它已经替你把各框架的版本冲突、初始化代码、目录结构都理顺了,自己从零拼装没准要多花三个小时,还不一定保证能跑通。我建议除非你对 ESLint 有强烈偏好,否则其他项全选 yes 就好。

生成后,复制环境变量文件:

cp .env.example .env

然后根据你的本地数据库情况修改DATABASE_URL。本地开发最省事的方式是先用 SQLite,把 prisma 的 provider 设为sqlite,连接串写成file:./db.sqlite。SQLite 不需要额外安装任何服务,跑完迁移就能预览效果;等部署到线上再切换到 PostgreSQL 也不麻烦,改一行 provider、改一个连接串就行。很多团队就是在本地 SQLite 上把功能全部验证完,再在 CI 里跑 PostgreSQL 测试,效果很好。

接着执行:

npx prisma migrate dev --name init npm run dev

浏览器打开http://localhost:3000,能看到首页有完整的样式和示例数据,说明项目基础已经跑通。如果这一步有任何报错,大概率出在.env文件没配好,可以直接跳到第 4 章的问题速查表去对号入座。

3.2 数据模型设计的完整示例

t3code的核心业务模型主要有:用户、文章、评论、点赞、关注关系。用 Prisma 写数据模型的体验非常直观,先看用户和文章的模型:

model User { id String @id @default(cuid()) name String? email String? @unique emailVerified DateTime? image String? accounts Account[] sessions Session[] posts Post[] comments Comment[] likes Like[] } model Post { id String @id @default(cuid()) title String content String published Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt authorId String author User @relation(fields: [authorId], references: [id]) comments Comment[] likes Like[] } model Comment { id String @id @default(cuid()) body String createdAt DateTime @default(now()) authorId String postId String author User @relation(fields: [authorId], references: [id]) post Post @relation(fields: [postId], references: [id]) }

这里 id 统一用cuid()而不是自增整数,主要考虑是分布式场景不容易撞 id,且公开接口不希望暴露业务量。实际项目里如果一开始用了自增 ID,之后想改成 cuid,会牵动大量外键,所以干脆在一开始就定好。

多对多关系是新手比较容易写懵的部分。比如用户和文章之间如果有"点赞"关系,需要显式定义一个中间模型:

model Like { id String @id @default(cuid()) userId String postId String user User @relation(fields: [userId], references: [id]) post Post @relation(fields: [postId], references: [id]) createdAt DateTime @default(now()) @@unique([userId, postId]) }

这个设计把"一个用户只能给同一篇文章点一次赞"写进了数据库层面,比单纯在业务代码判断更稳。为什么不在 User 和 Post 之间用隐式多对多关系?我建议显式建中间模型,因为之后大概率要加 createdAt、状态位这类字段,隐式表到时候改起来会麻烦得多。

修改完模型后,执行:

npx prisma migrate dev --name add_comment_and_like npx prisma generate

这里必须强调:prisma migrate dev只适合开发环境,它会把本地迁移文件和环境同步;线上数据库的更新应该用prisma migrate deploy。如果把它们混用了,最直接的后果是部署流程里出现一堆本地无关的迁移记录,甚至不小心把开发环境的脏数据同步到生产库。

3.3 tRPC 路由与完整鉴权实现

t3code在src/server/api/root.ts里把各个子路由合并成 appRouter,然后被 Next.js 的/api/trpc路由接管。以文章创建为例,核心代码:

import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; import { db } from "~/server/db"; export const postRouter = createTRPCRouter({ create: protectedProcedure .input( z.object({ title: z.string().min(1).max(100), content: z.string().min(1), published: z.boolean().default(true), }) ) .mutation(async ({ ctx, input }) => { return ctx.db.post.create({ data: { ...input, authorId: ctx.session.user.id, }, }); }), });

这段代码已经完整表达了三层业务约束:只有登录用户能创建文章,标题必须有内容且不超过 100 字,正文非空,当前用户 ID 来自服务端 session,而不是前端传参。最后这一点极其重要,如果依赖前端传用户 ID,任何有点经验的人都能伪造请求越权操作。直接在服务端从 ctx 里拿 session 里的 ID,是t3code从安全层面必须保证的。

列表查询同样简单:

list: publicProcedure .input(z.object({ onlyPublished: z.boolean().default(true) })) .query(async ({ ctx, input }) => { return ctx.db.post.findMany({ where: input.onlyPublished ? { published: true } : undefined, orderBy: { createdAt: "desc" }, include: { author: { select: { name: true, image: true } } }, }); }),

前端调用时:

"use client"; import { api } from "~/trpc/react"; import { useRouter } from "next/navigation"; export function PostCreateForm() { const router = useRouter(); const createPost = api.post.create.useMutation({ onSuccess: () => router.refresh(), }); return ( <form onSubmit={(e) => { e.preventDefault(); const formData = new FormData(e.currentTarget); createPost.mutate({ title: String(formData.get("title")), content: String(formData.get("content")), }); }} > <input name="title" className="border p-2" placeholder="标题" /> <textarea name="content" className="border p-2" placeholder="正文内容" /> <button type="submit" disabled={createPost.isPending}> {createPost.isPending ? "发布中..." : "发布文章"} </button> </form> ); }

这段代码里的isPending直接控制按钮 loading,提交成功后调用router.refresh()刷新服务端组件数据。整个过程没有手动发过一次 fetch、没有写过一行接口文档,前后端类型保持一致。这就是t3code的核心体验。如果你做的是纯客户端交互场景,useMutation返回的onSuccess里也可以做同样的数据刷新,保证页面数据始终以数据库为准。

3.4 登录注册与 Session 处理

登录是任何全栈项目都绕不开的模块。t3code接入的是 NextAuth.js,最省事的配置是 GitHub Provider。本地开发时在 GitHub OAuth Apps 里申请一个 client id 和 secret,填入.env就能登录。如果不想用第三方,也可以用 Credentials Provider,自己写用户名密码校验逻辑,但这样你得自己处理密码哈希、找回密码等流程,工作量并不小。我建议第一版先用 GitHub 登录验证业务,后续再考虑加更多 Provider。

配置的核心在src/server/auth.ts:

export const authOptions: NextAuthOptions = { session: { strategy: "jwt" }, providers: [GitHubProvider({ clientId: env.GITHUB_CLIENT_ID, clientSecret: env.GITHUB_CLIENT_SECRET })], callbacks: { jwt({ token, user }) { if (user) token.user = user; return token; }, session({ session, token }) { return { ...session, user: token.user }; }, }, };

为什么 session 策略用 JWT 而不是 database?对大多数t3code这类轻量产品来说,JWT 更简单、不额外依赖数据库 session 表,且 Vercel 这类 Serverless 环境下更容易扩展。如果你的产品要"踢人下线"、服务端 session 吊销这类强管理,再切回 database session 模式也不难。

session 与 tRPC 的配合是关键。createTRPCContext里调用getServerSession(authOptions),这样每个 tRPC 请求都会携带已登录用户信息。之后在protectedProcedure里直接解构ctx.session就能拿到用户 ID。这套方案比我在一些项目里见过的"自己写 JWT 中间件"干净得多,session 刷新、过期都由 NextAuth 管理,业务代码里只需要关心"当前用户是谁"。

前端判断是否登录,我通常用一个useSessionhook 包一下。但这里要强调一个容易写错的点:useSession是给客户端组件用的 hooks,服务端组件要判断登录状态应该用auth()方法,两者不能混用。t3code在src/server/auth.ts里导出了服务端用的auth(),也在src/trpc/init.ts里导出了客户端用的 session 相关类型。如果你在服务端组件里错误使用useSession,Next.js 会直接报错;反过来在客户端组件里 import 服务端模块,也会被server-only拦下来。这些约束看着严,其实是对你负责。

4. 常见问题与排查技巧实录

4.1 问题速查表

把t3code从开发到部署过程中我遇到过的、以及团队新同学经常踩的问题整理成下面这张表:

现象主要原因排查与解决
启动时报 Missing env variables.env 没复制或字段不全检查 DATABASE_URL、NEXTAUTH_SECRET、NEXTAUTH_URL 是否齐全
页面报 Server-only module imported by client component客户端组件引用了 server 目录模块把逻辑挪到服务端组件,或通过 tRPC 调用代替直接 import
tRPC 类型不更新router 修改后前端类型未重建重启 dev server,必要时rm -rf .next
修改 Prisma schema 后报找不到模型没执行 prisma generate每次 migrate 后执行npx prisma generate
NextAuth 登录后 session 为空Provider 回调或 session strategy 配置不对确认session.strategy = "jwt",并检查 callbacks 返回值
页面能打开但数据是空的数据库没跑迁移,或 Seed 没执行执行prisma migrate dev和prisma db seed
部署到 Vercel 后功能挂用了本地 SQLite 路径或漏环境变量切 PostgreSQL,重新配置线上环境变量
某次部署后数据库表对不上migrate dev 用在生产环境生产环境改用npx prisma migrate deploy

这张表的前三条几乎每个新接手t3code的人都会遇到,尤其第一条,通常不是配置有多难,而是忘了cp .env.example .env。把.env放进.gitignore已经是标配,所以在没有复制之前,项目启动就是必炸的。

4.2 三个值得单独展开的细节

第一个是.next目录缓存问题。tRPC 类型理论上不需要额外生成,但 dev server 对文件修改的监听有时会漏掉深层类型变化,导致前端拿到的还是旧类型。我自己处理这类问题有一套标准动作:先停止 dev server,再执行rm -rf .next,最后重新npm run dev。很多时候"重启大法"没用,是因为没有清缓存,问题出在旧的编译产物上。

第二个是 Seed 数据问题。第一次跑迁移后如果表里没有数据,列表页面会大面积白屏,容易被误认为代码 bug。我的做法是在prisma/seed.ts里创建两三个固定测试用户和一二十篇文章,然后执行:

npx prisma db seed

这样登录验证、分页测试、关注点赞这些操作都有真实数据支撑,开发效率高很多。

第三个是部署环境下的连接串问题。很多第一次部署的人把DATABASE_URL写成file:./db.sqlite,然后丢到 Vercel 上,线上自然找不到文件。t3code的部署建议是:本地用 SQLite 验证完功能后,生产环境切换 PostgreSQL;.env.production里单独维护连接串;部署前先本地执行一次npm run build,让 TypeScript 和 ESLint 在 CI 前把问题暴露出来。

说到后台上线,还有一个很多人忽略的点:Next.js 在构建时如果页面里有动态数据,千万别用静态导出(output: 'export')。t3code大量依赖服务端组件调数据库,必须让 Node.js 服务运行在 Vercel 或自己的服务器上。如果你在搭建时误开了静态导出,会出现"部署成功但页面全是 404"的怪现象。

5. 从本地到线上:部署与后续扩展方向

5.1 一键部署的完整路径

部署t3code到生产环境的最佳选择是 Vercel,因为 Next.js 和 Vercel 同生态,基本不用改代码。第一步先把数据库切到 PostgreSQL。如果不想自己维护数据库,Supabase 或 Neon 都提供免费层级的 Postgres,连接串复制到.env.production即可。第二步,把DATABASE_URL、NEXTAUTH_SECRET、NEXTAUTH_URL、GitHub OAuth 的 client id 和 secret 全部配置到 Vercel 的环境变量面板。第三步,设置构建命令为项目默认的迁移加构建流程,或者直接在 Vercel 的 Build Command 里写:

npx prisma migrate deploy && npx prisma generate && npm run build

注意NEXTAUTH_URL在线上一定要是真实域名,也就是https://yourapp.vercel.app,而不是http://localhost:3000。这个写错会导致 OAuth 回调失败,用户点击登录后跳转到奇怪地址,排查起来很费劲。如果你自己有服务器,用 Docker 部署也一样,把 Dockerfile 写成多阶段构建,用next start作为启动命令,数据库连接串同样来自环境变量。

部署完成后建议立刻跑一遍核心链路:注册登录、发文章、评论、点赞。别只在首页看一眼就以为万事大吉,我吃过这个亏:首页静态渲染正常,但登录后接口全部超时,最终发现是部署环境没把数据库迁移跑完。把核心链路本地验证一遍,能省掉一半"线上为啥比本地慢/挂"的疑问。

5.2 后续可以往哪个方向扩展

t3code的架构给后续扩展留了很自然的接口。想加文件上传,可以在 tRPC 里增加一个上传 mutation,把文件写入对象存储,然后保存 URL;想加全文搜索,直接用 PostgreSQL 的全文检索或外接一个搜索服务;想加通知系统,可以在 Prisma 里建一个 Notification 表,服务端在评论、点赞创建后写一条记录。

如果你想让t3code成为一个博客系统,还需要补上这几块:文章的 Markdown 渲染、SEO 元数据、站内搜索。如果你想把它做成社区产品,那要考虑审核机制和内容举报,这些都可以在现有 tRPC 路由上继续扩展,不会推翻整个架构。

我自己下一步的计划是给t3code加上一套基于角色的权限系统:普通用户、管理员、超级管理员。因为目前的 protectedProcedure 只区分"是否登录",而真实产品往往还要区分"是否有权限操作"。具体实现不复杂,可以在 protectedProcedure 之外再包一层 adminProcedure,在调用链路上多校验一次用户角色字段。这条加完之后,后台管理页面就能顺理成章地放开写,整个示例的完整度也会再上一个台阶。

6. 最后的实际操作体会

我在实际整理t3code的过程中最大的体会是:这种"全栈套件型"项目最值钱的地方,不是某段代码写得有多优雅,而是它把一堆优秀的工具用一种克制的方式组合在一起。你不需要理解每个框架的全部源码,只要顺着流程跑一遍,就能切实感受到端到端类型安全带来的踏实感。全栈项目最大的成本永远在维护,而维护成本的高低,往往在你写下第一行目录结构时就已经决定了。

最后再分享一个小技巧:如果你打算拿t3code当模板做自己的产品,第一件事不是打开编辑器改代码,而是把prisma/schema.prisma从头到尾读三遍。因为数据库模型就是业务边界,边界画错了,后面每一个页面都要跟着返工。先把模型设计得尽量贴合真实需求,再顺着 tRPC 的链路一层层往上补页面,比直接在组件里堆逻辑要高效得多。祝你把t3code改成你自己的样子,然后跑起来、部署出去、收到第一个真实用户。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进