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

T3 Stack实战:从tRPC到全栈类型安全的Next.js应用开发

T3 Stack实战:从tRPC到全栈类型安全的Next.js应用开发 ★ FEATURED ARTICLE
说实话第一次看到t3code这个项目名的时候我先是愣了一下随后就反应过来了——这十有八九又是奔着 T3 Stack 去的。如果你还没听说过 T3 Stack简单说就是目前全栈 Next.js 开发里相当顺手的一套组合拳Next.js TypeScript Tailwind CSS tRPC再配上 Prisma 和 NextAuth.js。t3code这个项目就是用这套技术栈从零到一搭建了一个技术内容分享平台支持用户注册登录、发布带标签的文章、在线编写 Markdown并且整套流程做到了从数据库到前端组件的全链路类型安全。这个项目最适合谁我觉得是两类人第一已经会写 React 但没接触过全栈想找个完整项目把前后端串起来的人第二用惯了 REST API想体验一下 tRPC 这种端到端类型安全开发模式的老前端。这篇文章我会把 t3code 从技术选型、项目初始化、数据建模、认证接入到部署上线的完整过程都拆开讲每一步都会附上我实际踩过的坑和调整过的细节保证你能直接照着思路复现。1. 项目设计与技术选型为什么 T3 Stack 值得认真用一次在动手写代码之前我花了大量时间纠结的不是功能怎么做而是技术栈怎么选。当时手上有几条路传统 Node Express React 前后端分离、Next.js REST API、Next.js GraphQL再有就是这次 t3code 最终采用的 T3 Stack。选型这件事不能只看流行度得看项目形态和团队习惯。1.1 T3 Stack 到底解决了什么痛点我最开始做全栈项目时用过 Express React 的组合前后端各写一套类型定义在接口文档里维护。今天后端改了个字段类型明天前端 TS 报错后天接口文档又忘了同步这种“类型断层”是真实存在的开发成本。REST API 不是不行而是它把类型安全的责任完全抛给了开发者。T3 Stack 的核心思路是用 tRPC 把前后端之间的 API 调用变成“像调用本地函数一样”的体验。什么意思你不需要手写 URL、不需要手写 fetch、不需要手写响应类型定义一切类型都从服务端 router 自动推导到前端。后端改了一个查错返回类型前端编译直接报警这就是 t3code 最终选择这条路的决定性理由。另外T3 Stack 整套方案都是 TypeScript 原生编写的。从数据库查询Prisma、到 API 层tRPC、再到前端组件React中间没有一条类型是断的。对我这种常年被类型问题折磨的人来说这种“一波传到底”的安全感很难拒绝。1.2 选型时那些被放弃的方案和原因我身边也会有人问为什么不直接用 NestJS或者直接用 Blitz.jsNestJS 高度工程化、模块体系完善但它更适合大型后端团队项目对于 t3code 这种以内容发布和展示为核心的场景来说有点“杀鸡用牛刀”而且 NestJS 和前端体系的类型联动依然要靠额外的接口文档才能衔接。GraphQL 我也认真考虑过。它的类型系统确实强大schema 即文档前端可以按需取字段。但代价是学习成本高、服务端复杂度大小项目很容易陷入“为了 GraphQL 而 GraphQL”的泥潭。T3 Stack 默认推荐的 tRPC 则轻量得多不需要额外定义 schema直接拿 TypeScript 类型当共享契约。顺便聊一下 Tailwind CSS 在 T3 Stack 里的位置。Tailwind 不是必须的 T3 Stack 文档里也强调你完全可以换成其他样式方案。但对于 t3code 这种需要频繁调整文章排版、标签样式、响应式布局的项目Tailwind 的工具类方式能显著减少“起类名、写 CSS 文件、切换文件”的烦琐感。而且 Tailwind 的 JIT 编译模式让最终产物体积非常可控这对部署上线后的首屏性能也有帮助。2. 环境准备与项目初始化从 create-t3-app 到跑起第一个页面T3 Stack 官方提供了脚手架命令create-t3-app这也是我建议任何新手第一次接触 T3 技术栈时的起点。手动搭建 Next.js tRPC Prisma Tailwind 的配置流程非常繁琐用脚手架能让你跳过配置地狱把精力集中在业务本身。2.1 初始化命令与交互选项说明我初始化 t3code 时用的命令是npm create t3-applatest t3code交互界面会让你选择要启用哪些模块。这里我逐项说一下怎么选Next.js默认勾选这是整个项目的地基。TypeScript默认勾选不选 T3 Stack 就失去灵魂了。Tailwind CSS我选择了启用快速搞定样式。tRPC默认勾选这是核心依赖。Prisma我选择了启用方便直接操作数据库。NextAuth.js我选择了启用后面做 GitHub 登录和 Google 登录都会用到。ESLint / Prettier都勾上团队协作和代码规范全靠它们。全部选完后脚手架会自动安装依赖并生成一个完整的基础项目。第一次跑npm run dev的时候会看到首页正常渲染并且已经集成了 tRPC 的健康检查接口那种立刻就有反馈的感觉对新手非常友好。2.2 生成后的目录结构与关键文件解读初始化完成后src目录结构是下面这样src/ pages/ // Next.js 页面路由 api/ trpc/ // tRPC 的 API 入口 auth/ // NextAuth 的 API 入口 server/ api/ routers/ // tRPC 路由 db.ts // Prisma 客户端实例 auth.ts // NextAuth 配置 styles/ utils/这个结构本身就是 T3 Stack 的最佳实践。所有“服务器端专属”的逻辑都放在server目录下前端不能直接 import 这里的文件避免把数据库连接、API 密钥等敏感内容暴露到浏览器端。pages/api 下的入口文件也非常薄只是把请求转发给 tRPC 处理器。有一点需要注意db.ts里 Prisma Client 的创建方式被特别处理了原因是在开发模式下 Next.js 热更新会不断创建新的实例导致数据库连接数被撑爆。脚手架生成的写法是这样的之后我们在开发时能少踩很多坑import { PrismaClient } from prisma/client; const globalForPrisma globalThis as unknown as { prisma: PrismaClient | undefined; }; export const prisma globalForPrisma.prisma ?? new PrismaClient(); if (process.env.NODE_ENV ! production) globalForPrisma.prisma prisma;3. 核心实现数据模型、tRPC 路由与页面开发t3code 的业务核心不算复杂用户注册登录、查看文章列表、创建带 Markdown 内容的文章、给文章打标签、支持按标签筛选文章。从功能划分来看核心难点集中在数据模型设计和 tRPC 路由的合理拆分。3.1 Prisma 数据模型设计与迁移注意事项t3code 的 schema 设计是这样的model User { id String id default(cuid()) name String? email String? unique emailVerified DateTime? image String? accounts Account[] sessions Session[] posts Post[] } model Post { id String id default(cuid()) title String content String published Boolean default(true) createdAt DateTime default(now()) updatedAt DateTime updatedAt author User relation(fields: [authorId], references: [id], onDelete: Cascade) authorId String tags Tag[] } model Tag { id String id default(cuid()) name String unique posts Post[] }因为启用了 NextAuthUser模型上的accounts和sessions关系是用于 OAuth 登录态存储的。我的Post通过authorId关联到用户并设置级联删除Post和Tag是多对多关系所以中间会有一个隐式关联表。在跑迁移的时候一定要养成“先比对再生成”的习惯npx prisma migrate dev --name init这一步会自动创建数据库表。如果后续修改了 schema建议每次都先跑npx prisma format格式化一下然后再迁移。另外npx prisma studio是本地调试数据库最直观的工具我强烈建议你在开发阶段多用用它比人肉 SQL 查数据快多了。3.2 tRPC Router 拆分与受保护路由实战t3code 的 tRPC 服务端代码放在src/server/api/routers下。我的拆法是一个资源一个 routerpostRouter文章列表、文章详情、创建文章、删除文章。tagRouter标签列表、创建标签、获取热门标签。userRouter获取当前登录用户的信息。postRouter创建文章的代码大致是这样的import { z } from zod; import { createTRPCRouter, protectedProcedure, publicProcedure } from ../trpc; export const postRouter createTRPCRouter({ list: publicProcedure .input( z.object({ tag: z.string().optional(), limit: z.number().min(1).max(50).default(20), }) ) .query(async ({ ctx, input }) { return ctx.db.post.findMany({ where: { published: true, tags: input.tag ? { some: { name: input.tag } } : undefined, }, include: { author: true, tags: true }, orderBy: { createdAt: desc }, take: input.limit, }); }), create: protectedProcedure .input( z.object({ title: z.string().min(1), content: z.string().min(1), tags: z.array(z.string()).default([]), }) ) .mutation(async ({ ctx, input }) { const post await ctx.db.post.create({ data: { title: input.title, content: input.content, authorId: ctx.session.user.id, tags: { connectOrCreate: input.tags.map((name) ({ where: { name }, create: { name }, })), }, }, }); return post; }), });注意protectedProcedure和publicProcedure的区别。list是公开查询任何人不需要登录都能看到文章create是受保护操作必须登录后才能调用。这个保护逻辑在 tRPC 底层已经实现前端调用时如果未登录服务端会直接抛UNAUTHORIZED错误不需要你再手写鉴权判断。3.3 前端数据获取React Query 下的 tRPC 写法前端调用 tRPC 的代码非常直观。比如拉取文章列表只需要这样import { api } from ~/utils/api; export default function HomePage() { const { data: posts, isLoading } api.post.list.useQuery({ limit: 20 }); if (isLoading) return divLoading.../div; return ( div {posts?.map((post) ( article key{post.id} h2{post.title}/h2 p{post.author.name}/p /article ))} /div ); }你不需要手动写 fetch、管理 loading state、处理错误状态useQuery返回的数据字段会自动匹配服务端返回的类型。创建文章时用useMutation也相当顺畅const ctx api.useUtils(); const createPost api.post.create.useMutation({ onSuccess: () { ctx.post.list.invalidate(); }, }); createPost.mutate({ title: 我的第一篇文章, content: 正文内容, tags: [React, T3], });有一个细节值得提useMutation完成后调用invalidate让 React Query 重新拉取最新列表这样新文章出现时页面不会出现“数据滞后”的错觉。如果你对状态管理还停留在 Redux 时代可能会觉得每个页面都要手动管理 store 很麻烦但这里完全没有那个负担。4. 认证体系集成NextAuth.js 与多提供商 OAuth 接入t3code 的登录功能我接了两个 OAuth 提供商GitHub 和 Google。这里最容易被新手忽略的不是按钮怎么放而是 NextAuth 的 Session 策略和 Prisma 的适配关系。4.1 NextAuth 配置与 OAuth 应用创建过程先在 GitHub 的 Developer Settings 里创建一个 OAuth App回调地址填http://localhost:3000/api/auth/callback/githubGoogle Cloud Console 那边同样创建一个 OAuth Client回调地址填http://localhost:3000/api/auth/callback/google然后把对应的 Client ID 和 Client Secret 放进.envGITHUB_CLIENT_IDxxxx GITHUB_CLIENT_SECRETxxxx GOOGLE_CLIENT_IDxxxx GOOGLE_CLIENT_SECRETxxxx NEXTAUTH_SECRETxxxx NEXTAUTH_URLhttp://localhost:3000src/server/auth.ts里的关键配置大概是import { getServerSession, type NextAuthOptions } from next-auth; import GitHubProvider from next-auth/providers/github; import GoogleProvider from next-auth/providers/google; import { PrismaAdapter } from next-auth/prisma-adapter; import { prisma } from ./db; export const authOptions: NextAuthOptions { adapter: PrismaAdapter(prisma), providers: [ GitHubProvider({ clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, }), GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, }), ], session: { strategy: database }, callbacks: { session: ({ session, user }) ({ ...session, user: { ...session.user, id: user.id }, }), }, };session 策略这里我用了database。这表示 Session 数据存到数据库的Session表里是 T3 Stack 搭配 Prisma Adapter 的默认方式用户退出登录后能立即让会话失效安全可控性比 JWT 策略高一些。4.2 登录状态在前端的渲染与权限控制NextAuth 提供useSession钩子但更推荐的是服务端通过getServerSession直接获取登录状态这样首屏渲染时就不会出现“闪一下未登录状态”再变成已登录的情况。t3code 页面里的判断逻辑是这样的import { getServerSession } from next-auth; import { authOptions } from ~/server/auth; export async function getServerSideProps() { const session await getServerSession(authOptions); return { props: { session } }; }页面组件里就对 session 做分支渲染未登录时显示登录按钮已登录时显示发布入口和用户头像。我在实际开发中遇到过一个常见误区很多人在客户端直接用next-auth/react的signIn弹出 OAuth 窗口但在本地开发环境中回调地址配置漏掉了端口号导致跳转后 404。这类问题排查的思路很简单——先去查看 NextAuth 的日志输出确认它实际接收到的回调 URL 长什么样再对比 OAuth 后台配置是否一致。5. 部署上线与性能优化实测本地开发跑通后紧接着就是部署。t3code 我选择部署到 Vercel原因其实很直接Next.js 生态与 Vercel 的适配最好几乎不需要额外的服务器配置。如果你追求省钱或者更灵活的控制权也可以考虑自建 VPS但那就需要自己处理 Node 进程守护、Nginx 反向代理和 HTTPS 证书配置成本明显更高。5.1 Vercel 部署流程与数据库迁移注意事项部署前要先去 Vercel 创建项目然后把本地仓库关联过去。环境变量必须在项目设置里逐项配置DATABASE_URL、NEXTAUTH_SECRET、NEXTAUTH_URL、GITHUB_CLIENT_ID、GITHUB_CLIENT_SECRET等等一个都不能漏。生产数据库我用的是 PostgreSQL Prisma迁移这一步需要特别小心。本地开发跑prisma migrate dev会自动同步开发库但线上环境不能直接跑迁移需要在构建命令里加上npx prisma generate npx prisma db push这里用db push而不是migrate deploy的原因是对于内容发布平台这种小型项目表结构一般不会频繁变更db push足够直接。如果是大型多环境协作项目我还是推荐用migrate deploy确保迁移历史可追溯。5.2 性能优化与 Lighthouse 实测数据t3code 部署完成后我用 Lighthouse 做了一轮检测首屏性能初始分数大约在 72/100调整后稳定在 90。主要做了三件事第一是图片优化。Next.js 自带的next/image组件对文章封面图非常有用它会自动做响应式尺寸处理、WebP 格式转换和懒加载。不要用普通img标签加载图片这一项对 LCP 影响非常明显。第二是静态页面缓存策略。对于文章列表、标签列表这类不常变化的数据我用getStaticProps配合revalidate做了 ISR 增量静态再生每 60 秒重新生成一次既保证了内容新鲜度又做到 CDN 缓存。第三是减少客户端 JS 体积。tRPC 的客户端代码是按需打包的但如果你不小心在组件里直接引用了server/下的代码就会把很多服务端依赖带进浏览器。这个坑我有一次真的踩到了首屏加载了一个 300KB 的额外 chunk后来日常自查时会专门使用生产构建观察页面加载的 chunk 列表。6. 开发实录 t3code 项目里的常见问题与排查技巧任何项目实际开发过程都不会一帆风顺。t3code 开发中我也遇到了一些值得记录的问题这些问题在 Stack Overflow 上反复有人问但官方文档往往不会直接告诉你怎么破。6.1 高频问题与解决方案速查问题现象可能原因解决方法Prisma 报P1001无法连接数据库本地 PostgreSQL 没启动或DATABASE_URL填错用pg_isready检查数据库状态核对连接串tRPC 调用时代码提示UNAUTHORIZED前端没带 Session或未登录就访问保护路由调用前检查前端session是否为空或改用 publicProcedureNextAuth 跳转后回来报callback URL mismatchOAuth 回调地址没配置好或NEXTAUTH_URL和请求地址不一致把本地回调地址精确到端口号生产配置和线上域名保持一致首次构建时prisma generate报找不到 schema构建命令没有在 Prisma CLI 的目录下执行构建命令改成npx prisma generate npx next build热更新后页面一直 Loading 不加载数据tRPC 服务端返回异常或 React Query 缓存异常浏览器控制台查看 Network 面板确认 tRPC 请求是否返回 500TypeScript 报没有.env类型定义环境变量没有在src/env.mjs里声明在 env schema 里补充对应的z.string()定义6.2 我觉得最有用的几条开发体验关于 t3code 的开发过程有几点体会让我印象深刻。第一点是“类型安全不等于零成本”。tRPC 让前后端类型终于能一管到底为你省去了手写接口文档和联调的消耗但对应的代价是它要求你规范好 router 的边界——一旦 router 设计得混乱类型推断反倒会让你在排查时多绕几圈。建议新项目从小而清晰的多个 router 开始不要把所有 query 和 mutation 堆在同一个文件里。第二点是开发环境的数据库尽量用本地 PostgreSQL而不是 SQLite。虽然 SQLite 不需要额外服务就能启动但 Postgres 和 SQLite 在 Prisma 的行为上存在细微差异尤其是事务和并发处理逻辑反正都要上线到 Postgres本地就用一样的数据库可以减少很多“本地正常、线上报错”的尴尬。第三点是保护好你的NEXTAUTH_SECRET。本地开发时候随手生成的 key一旦传到公开仓库恶意的人就能伪造 Session 信息。t3code 项目启动后我第一时间把 git 历史里的.env清掉了然后生成了新的 secret这个操作在多人协作项目或者开源项目里尤其重要。第四点也是我最想提醒你的t3code 这种 T3 Stack 项目真正扎根起来后你会发现对前端框架的固有认知会被重新洗一遍。以前你可能习惯说“前端调接口、后端管数据”但现在你可以直接把一个带有数据库查询和鉴权判断的函数写进前端调用的依赖树中服务端代码和客户端代码之间的边界从一个网络接口变成了一个类型导入声明。这种体验上的变化可能比任何技术选型的数据都要来得真实、来得有力度。最后再分享一个小技巧如果你是第一次用 t3code 这类 T3 项目练手建议把官方create-t3-app生成的基础模板完整地跑通一遍包括登录、请求数据、迁移数据库再开始改造业务。很多人一上来就删掉模板里的示例代码反倒把最关键的配置链路搞断了后面每一步都走得很痛苦。先让默认的“会呼吸”的最小系统跑起来后面再加你的业务逻辑一切都顺很多。
阅读完成 · 觉得有帮助?
咨询建站