news 2026/10/28 7:14:06

T3 Stack实战:用Next.js、tRPC、Prisma打造端到端类型安全的全栈应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
T3 Stack实战:用Next.js、tRPC、Prisma打造端到端类型安全的全栈应用

上个月我在重构一个内部工具时,被前后端联调的类型问题折磨得够呛。后端接口返回的字段改了名,前端编译期毫无反应,只有运行到那一页才发现数据渲染不出来。后来我把整套代码迁移到 t3code 这个项目模板上,问题算是从根上解决了。t3code 是我自己维护的一套全栈 TypeScript 脚手架,核心是 Next.js、tRPC、Prisma、Tailwind CSS 和 NextAuth 的组合,它解决的就是全栈应用里最常见的几个痛点:类型安全、开发效率和认证集成。如果你正在搭一个新项目,又不想从零开始磨基础设施,这篇文章应该能给你一些参考。

1. t3code 到底是什么?为什么我会自己写一个脚手架

1.1 一个真实的起步场景

先说背景。我当时要做一个仓库盘点小程序,功能其实不复杂:登录、商品管理、盘点单、简单报表。但小程序的后台需要有人管理,前端要做商品列表和扫码录入,后端要处理数据和权限。如果按以前的习惯,用 React + Vite 搭前端,再用 Express 写一套 REST API,那么同一个数据模型就得在数据库、后端 DTO、前端接口类型里各写一遍。一旦调整字段,三处都要同步,漏掉一个就得靠运行时去发现。

t3code 这个项目就是在这种背景下开始的。我需要一个模板,它不能太重,不能像某些企业级框架一样把目录结构限定得很死,但必须把 TypeScript 的类型从数据库一路打通到页面组件。说白了,我想要"改一处 schema,全项目跟着变"的效果。

1.2 T3 Stack 到底在解决什么问题

T3 Stack 并不是什么官方框架,它是社区里对一套"类型安全优先"的技术组合的称呼,来自开发者 Theo 提出的想法。严格来说,T3 这个名字强调的就是 TypeScript、端到端类型安全和可预测的开发体验。t3code 的核心就是围绕这套思想做的选型。

这套组合里,Next.js 负责页面和路由,tRPC 负责 API 调用,Prisma 负责数据库建模和访问,Tailwind CSS 负责样式,NextAuth 负责认证。它们各管一摊,但通过 TypeScript 的类型系统连成一条线。你定义好 Prisma 的模型,Prisma 会生成对应的类型;你在 tRPC 的 router 里写查询函数,这个函数的参数和返回值类型会自动暴露给前端;前端调用时不需要手写接口文档,也不需要手动指定类型,编辑器会自己告诉你参数是什么、返回什么。

这里有一个很关键的体验差异。用 REST + axios 的时候,后端返回的{ id, name }到了前端只是一个any,你只能靠记忆去猜字段名。而 tRPC 的做法更像调用本地函数,参数和返回值类型都是直接可见的。这种体验在团队协作时尤其明显:后端改了返回结构,前端一编译就报错,而不是等页面白屏才有人发现。

1.3 设计边界:不是全家桶,是精选组合

我见过不少人一提到"脚手架"就想到全家桶,明明只是写个博客,却把微服务、消息队列、Redis 缓存全都塞进来。t3code 在设计上刻意做了减法。

它默认不包含组件库,因为 UI 风格是最容易因人而异的;它不强制你使用某种状态管理库,因为在 React Server Components 和 tRPC 的配合下,大部分页面根本用不到全局状态;它也不预装表单库,因为很多小需求用原生表单就能解决。t3code 只保证一件事:让你用最少的配置,跑起来一个具备登录、数据库、API、页面渲染能力的全栈应用。

这样的好处是学习成本低。你看到的每个文件都是必要的,没有多余的概念。对新手来说,研究模板源码也比研究一堆抽象包装来得更快。对老手来说,想替换某个模块也很方便,因为依赖关系是清晰的,不是缠成一团。

2. 核心细节解析:t3code 的五个组成模块

2.1 Next.js:页面、路由与中间件的选择

t3code 默认使用 Next.js 的 App Router 模式,而不是过去的 Pages Router。这不是追新,而是 App Router 的 Server Components 思路更适合搭配 tRPC。

在 App Router 里,页面组件默认是服务端组件,可以直接访问数据库,不需要通过 API 绕一圈。但有些交互逻辑必须发生在客户端,这时候就需要在文件顶部写"use client"。t3code 的约定是:只要组件里用了 tRPC 的 hooks,就把它标记为客户端组件。比如一个商品列表页,外壳是服务端组件负责布局,列表部分是客户端组件负责调数据,这样语义清晰,也不会让整个页面变成客户端渲染。

路由方面,App Router 的文件约定很简单:app/page.tsx是首页,app/login/page.tsx是登录页,app/api/trpc/[trpc]/route.ts是 tRPC 的 HTTP 入口。如果没有特殊需求,几乎不需要手动写路由表。中间件部分,Next.js 本身提供middleware.ts,t3code 用它来做简单的登录态检查,比如没登录就重定向到/login。注意中间件里不能直接读取数据库,只能处理 cookie 和请求头级别的信息,复杂的权限校验要放到 tRPC 的 procedure 里做。

2.2 tRPC + Prisma:类型安全从数据库到前端的通路

tRPC 这套东西第一次用的时候会觉得有点反直觉,因为它不生成 URL,也不生成 REST 风格的接口。你写好一个 router,前端导入的是一个createTRPCClient实例,然后直接在上面调client.post.list(),就像调用一个普通函数。实际上,这个函数会通过 HTTP 请求访问后端的同一个 router。

t3code 的 tRPC 配置分成三层。第一层是server/api/routers,这里写具体的业务逻辑,比如查询帖子列表、创建用户、更新状态。第二层是server/api/root.ts,把所有的子 router 合并成一个根 router。第三层是客户端trpc/react.tsx,创建 React hooks 的 Provider,让前端页面能直接调用。

Prisma 在这个链路里的位置比较特殊,它更像一个 TypeScript 代码生成器。你在schema.prisma里写model Post { id Int @id @default(autoincrement()) title String content String },执行npx prisma generate之后,Prisma Client 就会生成一个Post类型,这个类型可以直接用在 tRPC router 的返回值上。因为 tRPC 的 infer 机制,这个返回值的类型会一路传到前端,前端拿到post对象时,编辑器能提示出post.title和post.content,不会多一个字段,也不会少一个字段。

2.3 NextAuth.js:登录会话的集成方式

认证是全栈应用里最容易做乱的部分。t3code 选 NextAuth.js 而不是自己写 JWT 签发是因为它把主流登录方式都封装好了,GitHub、Google、邮箱验证码都可以配。更重要的是,它和 Next.js 的 App Router 集成得非常顺滑。

在 t3code 里,登录相关的核心文件是app/api/auth/[...nextauth]/route.ts。NextAuth 会把整个认证流程拆成几个 route handler,你只需要导出一个handler给 GET 和 POST 请求就行。另一个关键文件是server/auth.ts,它负责把 NextAuth 的 session 信息提供给 tRPC 的 context,这样你在 tRPC procedure 里就能通过ctx.session.user拿到当前登录用户。

这里有两个细节容易踩坑。第一,如果你需要用户的完整资料,比如头像、角色、手机号,那就别用默认的 JWT session 模式,改成数据库 session。在authOptions里加上adapter: PrismaAdapter(prisma),再在 schema 里加上Account、Session、VerificationToken这几个模型,登录信息就会存到数据库。第二,客户端获取 session 不推荐在任意组件里直接调用useSession,因为那样会频繁触发请求,最好在顶层 Provider 里通过SessionProvider管理,页面组件再从 context 里取。

2.4 Tailwind CSS:样式系统的取舍

样式方案我曾经纠结过,试过 CSS Modules,也试过 styled-components,最后 t3code 还是选 Tailwind CSS。原因很简单:在模板项目里,Tailwind 的工具类能让我不离开 HTML 结构就把样式写完,而且它和 React 组件的组合几乎是零成本。

t3code 默认带了一套简单的全局样式,字体、颜色变量、基础间距都在src/styles/globals.css里。组件变化不频繁,所以我没有预置复杂的设计系统,比如按钮、卡片、弹窗这些组件都留给使用者自己写。如果你想直接用现成的,可以在此基础上接一个组件库,Tailwind 写的类名和大多数组件库并不冲突。

Tailwind 的版本建议锁定 v3 系列。v4 改成 CSS-first 配置方案,有一些新写法,但考虑到生态和插件兼容性,模板里固定用 v3 更稳。如果你后面对主题定制有需求,在tailwind.config.ts里扩展theme即可,这里要注意设置content数组,把./src/**/*.{ts,tsx}包含进去,否则写的新类名不会被编译出来。

2.5 目录结构与配置文件的组织

一个项目的幸福感,很大程度上取决于目录结构是不是一眼能看懂。t3code 的目录结构刻意保持扁平,顶层是三个目录:app放页面和路由,server放后端逻辑,src放公共代码和样式。实际结构大概是这样的:

. ├── app │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ └── trpc │ │ └── [trpc] │ ├── layout.tsx │ └── page.tsx ├── prisma │ └── schema.prisma ├── src │ ├── components │ ├── styles │ └── lib ├── middleware.ts ├── trpc │ ├── client.ts │ ├── react.tsx │ └── server.ts ├── .env └── package.json

trpc目录不是必须的,但把它独立出来会让依赖关系更清楚。server/auth.ts挂在server目录下,它同时被/api/auth和 tRPC context 引用,集中管理认证相关逻辑。src/lib里通常放工具函数,比如格式化时间、格式化金额。整体原则是:别让任何文件承担它名字以外的职责,否则项目大了以后,找一个问题要翻遍整个仓库。

3. 实操过程:用 t3code 从零搭建一个带登录的示例应用

3.1 环境准备与初始化

开始之前,确认你本机有 Node.js 18.18 以上版本,以及包管理器 pnpm 或 npm。我推荐 pnpm,因为依赖安装速度和磁盘占用都更友好。初始化命令很简单:

npx t3code@latest init my-app cd my-app

初始化过程会问你要不要启用 NextAuth、Prisma、Tailwind,按需勾选就行。如果你一个人用,不需要复杂的认证流程,可以先选一个简单的邮箱登录。安装完依赖后,第一件事是把.env.example复制成.env,并填上必要的变量。Prisma 默认连接 SQLite,所以数据库 URL 一般是DATABASE_URL="file:./dev.db",只用这一个变量就能跑起来。如果你要用 PostgreSQL,就改成DATABASE_URL="postgresql://user:password@localhost:5432/mydb"。

这里提醒一下,.env文件里的NEXTAUTH_SECRET是要保密的。开发环境里你可以先随便填一个长字符串,生产环境必须用真正的随机值,可以用openssl rand -base64 32生成。

3.2 数据模型与数据库迁移

假设示例应用是一个记账本,我们定义三个模型:用户、账单分类、账单记录。先打开prisma/schema.prisma,写下模型:

model User { id String @id @default(cuid()) email String @unique name String? bills Bill[] } model Category { id String @id @default(cuid()) name String color String user User @relation(fields: [userId], references: [id]) userId String } model Bill { id String @id @default(cuid()) amount Decimal @db.Decimal(10, 2) note String? date DateTime @default(now()) category Category @relation(fields: [categoryId], references: [id]) categoryId String user User @relation(fields: [userId], references: [id]) userId String }

写完 schema 后,执行:

npx prisma migrate dev --name init

这个命令会把模型变化生成 SQL 迁移文件,并同步创建数据库表。一个容易忽略的点是Decimal类型,如果你在数据库里存金额,别用Float,浮点数会有精度问题。映射成 TypeScript 后,Bill.amount的类型不是number而是Decimal,需要手动调用toNumber()转一下。

3.3 实现第一个 tRPC 查询

接下来在server/api/routers下新建bill.ts,先写一个返回当前用户所有账单的查询:

import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "../trpc"; import { prisma } from "~/server/db"; export const billRouter = createTRPCRouter({ list: protectedProcedure.query(async ({ ctx }) => { return prisma.bill.findMany({ where: { userId: ctx.session.user.id }, include: { category: true }, orderBy: { date: "desc" }, }); }), });

注意这里用了protectedProcedure,表示只有登录用户才能访问。ctx.session.user.id是由认证中间件注入的,不需要你自己从请求里解析用户身份。

然后在server/api/root.ts里挂载这个 router:

import { billRouter } from "./routers/bill"; export const appRouter = createTRPCRouter({ bill: billRouter, });

写完后启动开发服务:

npm run dev

打开浏览器,你可能会想直接访问接口地址,但 tRPC 的接口路径不是给人手敲的。它的 endpoint 长这样:/api/trpc/bill.list,直接访问会看到一个 JSON 响应,说明链路已经通了。

3.4 接入认证与页面鉴权

前端页面若想调用刚才的查询,需要先包装 SessionProvider 和 tRPC Provider。在app/layout.tsx里做一次全局包裹:

import { TRPCReactProvider } from "~/trpc/react"; import { SessionProvider } from "next-auth/react"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="zh-CN"> <body> <SessionProvider> <TRPCReactProvider>{children}</TRPCReactProvider> </SessionProvider> </body> </html> ); }

注意SessionProvider本身是客户端组件,但可以放在服务端 layout 里包裹 children,Next.js 会正确渲染。然后在需要数据的地方,新建一个客户端组件:

"use client"; import { api } from "~/trpc/react"; export function BillList() { const { data: bills, isLoading } = api.bill.list.useQuery(); if (isLoading) return <div>加载中...</div>; return ( <ul> {bills?.map((bill) => ( <li key={bill.id}> {bill.amount.toNumber()} - {bill.note} </li> ))} </ul> ); }

页面鉴权方面,能在服务端做的就别在客户端做。我们可以在middleware.ts里拦截未登录请求:

import { withAuth } from "next-auth/middleware"; export default withAuth({ pages: { signIn: "/login" }, }); export const config = { matcher: ["/dashboard/:path*"], };

这样未登录用户访问/dashboard会被自动带到登录页。注意中间件只做基础跳转,具体某个按钮、某条接口是否允许访问,还是要靠 tRPC 的protectedProcedure把关。

3.5 构建与部署的完整链路

本地开发没问题后,下一步是部署。t3code 可以部署到 Vercel,也可以放到自己的服务器上跑 Docker。如果你用 Vercel,只需在控制台导入 Git 仓库,填好环境变量DATABASE_URL和NEXTAUTH_SECRET,它会把 Next.js 当作 Serverless 函数来跑。需要注意的一点是:Serverless 环境下 Prisma 会冷启动,最好在lib/db.ts里用全局变量缓存 PrismaClient,避免每次请求都重新创建连接。

如果是自建服务器,我推荐写一个简单的 Dockerfile:

FROM node:20-alpine AS base WORKDIR /app COPY pnpm-lock.yaml ./ RUN corepack enable && pnpm install --frozen-lockfile COPY . . RUN npx prisma generate && npm run build CMD ["npm", "run", "start"]

这里有一个坑:prisma generate必须在构建阶段执行,而且是在npm run build之前。如果顺序反了,Next.js 构建时会提示找不到 Prisma Client。部署前还要先执行prisma migrate deploy让生产数据库应用迁移文件,注意生产环境不要用migrate dev,那个命令是为了本地开发设计的。

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

4.1 类型不匹配:tRPC 与 Prisma 联调时的经典报错

新手最容易遇到的报错是类型冲突。比如你在billRouter里写了const result = await prisma.bill.findMany(),然后在 return 里给amount做了toNumber(),但是 tRPC 的输入输出类型推断依然要求返回值是Decimal。解决办法简单粗暴:要么保持Decimal类型不动,前端再转换;要么在返回前 map 成 DTO 对象,显式标注类型。

还有一个更隐蔽的问题:当你在prisma查询里用了include: { category: true },但实际某个 bill 没有关联 category,Prisma 的写法要求用category: { include: ... }或者category: true保持类型一致。如果用include: { category: { select: { id: true, name: true } } },它返回的类型就会是只有这两个字段的联合类型,前端访问bill.category.color就会编译报错。排查这类问题不要靠肉眼,直接在编辑器里 hover 类型看提示。

4.2 Prisma Client 版本不一致

改完 schema 之后忘了执行npx prisma generate,是另一个高频问题。你可能会发现数据库操作仍然能跑,但 Prisma Client 不识别新字段。执行npx prisma generate后,如果还有类型报错,可以删除node_modules/.prisma和node_modules/@prisma/client再重新安装,这种极端操作能解决大部分因为缓存导致的奇怪问题。

在我的项目里,prisma 和 @prisma/client 的版本必须保持一致,package.json里最好把版本号绑定成同一个。升级 Prisma 的时候,不要只升其中一个包,否则会报prismaCLI 和 generator 版本不一致的错。你可以在package.json里同时修改两者,然后重新跑npx prisma generate。

4.3 部署后的环境变量与数据库连接

本地跑得好好的,一部署就连不上数据库,这是环境变量的问题。常见原因有三个。

第一是.env文件没有上传到服务器。如果你用 Docker,构建时默认不会把.env复制进去,需要在 Dockerfile 里用ARG和ENV显式传递,或者在启动命令里用环境变量替换。第二是数据库连接串写错了,比如把本地localhost地址写进了生产环境。第三是防火墙或者数据库白名单没开,尤其是云数据库,通常只允许特定 IP 访问。

排查顺序建议是:先确认DATABASE_URL能不能从服务器上正常连接,可以临时用psql或mysql客户端测试;再确认 Prisma 是否能在构建阶段读取到环境变量,因为 Next.js 的server环境和环境变量加载顺序有关。如果构建时 Prisma 找不到数据库,可以考虑在构建命令前加set -a && source .env && set +a这类加载方式。

4.4 性能与安全细节

类型安全不代表不需要做性能优化。使用 tRPC 后你会发现,每个 procedure 默认是一个 POST 请求,虽然方便,但如果你在一个页面里调了七八个查询,请求数就会偏多。t3code 的模板里没有内置 batch 配置,其实 tRPC 默认支持 HTTP Batch,你需要把createTRPCClient的 links 数组里加上httpBatchLink。在 React 组件里,多个useQuery发起的请求会合并成一个,这能明显减少首屏等待时间。

安全方面,最常见的问题是把敏感数据塞进 tRPC 返回值。因为 tRPC 的类型会暴露给前端,你写的每个返回值都等于公开了字段结构。所以prisma查询里要记得用select或omit排除密码哈希、邮箱等敏感信息。还有一个容易被忽视的点是 tRPC 的错误信息,生产环境不要直接返回数据库原始错误,否则会泄露表结构。建议在createTRPCContext里处理onError,只记录日志,对客户端返回通用的INTERNAL_SERVER_ERROR。

4.5 避坑清单速查表

下面这些坑是我实际踩过的,整理成一张表,可以快速对照排查:

问题现象可能原因解决方式
页面提示PrismaClient is not definedPrisma Client 未生成或缓存异常执行npx prisma generate,删除 node_modules 后重装
tRPC 查询参数类型报错查询不需要参数却传了undefined使用useQuery(undefined)而不是useQuery()
NextAuth 登录后跳转回首页而非原页面未配置callbacks.redirect在authOptions里加 redirect 回调
部署后访问 API 返回 404环境变量NEXTAUTH_SECRET缺失或路径错误检查.env和生产环境变量配置
Tailwind 样式不生效content 配置没包含对应的目录检查tailwind.config.ts的content数组
前端看不到最新数据tRPC 查询未触发重新获取在 mutation 成功后调用utils.bill.list.invalidate()

排查问题的时候,我习惯先把报错信息复制到编辑器里跟着类型走一遍,再看运行时日志。tRPC 的报错通常已经足够精确,它会把 procedure 名、错误位置和上下文都打出来,大多数情况下照着提示改就行。

最后再说几句关于 t3code 的体会

开发 t3code 这个模板的过程,比我预想的要长。最初我只是想给自己搭一个舒服的开发环境,结果越用越觉得应该把那些反复写的初始化文件固定下来。后来我把它整理成开源项目,收到过一些 PR,也收到过不少 issue。印象最深的是有个开发者把模板里的 tRPC 改成 gRPC,然后又跑回来说还是 tRPC 这个方案更合适。这不是说 gRPC 不好,而是 t3code 这套"端到端类型安全"的思路确实能省很多事。

如果你正在考虑要不要用 t3code 或者类似的 T3 Stack 方案,我的建议是别急着全部照搬,先拿一个小功能试试。比如写一个带登录的待办清单,感受一下"改 Prisma schema,然后全链路类型跟着变"的体验。如果你习惯写明文的 REST 接口,可能刚开始会被 tRPC 的抽象绕一下,但用了两三天之后,你会开始讨厌手写接口类型。最后,记得给 t3code 这种个人脚手架留点改动空间,因为它最重要的意义不是帮你定死规则,而是给你一个可以随时改造的起点。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 7:14:04

从零构建t3code:基于Tauri的本地代码片段生成工具实践

我花了差不多一个月的晚上&#xff0c;把t3code这个工具从想法变成了我能天天用的东西。起因特别简单&#xff1a;我发现自己总是复制粘贴同一段代码——新建一个 REST 接口、初始化一个 Vue 组件、写一个带分页的列表查询。每次都要去翻旧项目&#xff0c;找到差不多的写法&am…

作者头像 李华
网站建设 2026/10/28 7:13:09

Flutter与OpenHarmony音乐播放器专辑详情开发指南

1. 项目背景与需求分析在移动应用开发领域&#xff0c;音乐播放器始终是一个经典且具有挑战性的项目类型。随着Flutter框架的不断成熟和OpenHarmony操作系统的崛起&#xff0c;将两者结合开发音乐播放器App成为许多开发者的新选择。本次我们要实现的是音乐播放器中至关重要的&q…

作者头像 李华
网站建设 2026/10/28 7:11:53

4位OTP单片机语音智能垃圾桶实战项目

简介&#xff1a;本资源是一套基于STM32单片机的智能垃圾桶完整嵌入式开发工程&#xff0c;面向电子类专业学生、嵌入式初学者及课程设计实践者&#xff0c;聚焦垃圾分类场景下的传感器融合、语音交互与低功耗控制等核心问题。压缩包共226个文件&#xff0c;含41个.h头文件与37…

作者头像 李华
网站建设 2026/10/28 7:10:52

RankEvolve:面向可复现科研的多智能体自动研究框架

1. RankEvolve 不是“又一个AI框架”&#xff0c;而是研究范式的结构性迁移最近刷到“Meta发布RankEvolve多智能体自动研究框架”这个标题&#xff0c;很多人第一反应是&#xff1a;哦&#xff0c;Meta又出新模型了&#xff1f;点开一看却发现没有模型权重、没有SOTA榜单、甚至…

作者头像 李华
网站建设 2026/10/28 7:10:49

RankEvolve:面向可验证科研的多智能体自动研究框架

1. 这不是又一个“智能体玩具”&#xff0c;而是研究范式的底层迁移RankEvolve这个名字乍听像某个新出的AI模型代号&#xff0c;但如果你翻过Meta AI官网那篇不到2000词的论文预印本&#xff0c;会发现它根本没提“模型”二字——它连一行训练代码都没放。我第一时间下载了他们…

作者头像 李华
网站建设 2026/10/28 7:10:36

MATLAB实现LSTM-Attention时序预测:工业设备RUL建模实战

简介&#xff1a;本资源是一份面向深度学习初学者与MATLAB工程实践者的回归预测实战方案&#xff0c;聚焦多输入单输出时序建模任务&#xff0c;解决工业传感数据、金融时间序列或环境参数等场景下的高精度连续值预测问题。压缩包共12个文件&#xff0c;含11个核心MATLAB函数&a…

作者头像 李华