资讯动态

t3code:基于T3 Stack的全栈TypeScript脚手架实践

发布时间:2026/10/27 7:02:19 来源:尧图企业网站定制
1. 项目整体设计与思路拆解1.1 t3code 到底是什么t3code 是我近期基于T3 Stack搭建的一套全栈 TypeScript 项目脚手架。T3 Stack 是由 Theo 社区推动的一套现代化 Web 开发技术栈组合核心成员是 Next.js、TypeScript、tRPC、Tailwind CSS再配合 Prisma 和 NextAuth.js 等周边生态形成了一条从数据库到页面渲染全程类型安全的开发链路。我自己在接触 T3 Stack 之前前后端分离的项目做过不少典型的痛点就是接口文档维护成本高、前后端数据结构经常对不上、联调时反复扯皮。t3code 这套脚手架想解决的恰恰是让类型从数据库一直贯穿到浏览器端这个问题——后端 Prisma 定义的模型类型通过 tRPC 的 procedure 暴露给前端前端调用接口时能拿到完整的类型提示和编译期校验不用再单独维护一份手写的 API 类型定义。这个项目适合谁参考我觉得至少有三类人能从里面拿到东西一是被前后端类型割裂折磨的中级前端工程师想体验一把全栈类型安全的开发方式二是想快速搭一个带数据库、带登录鉴权的 Next.js 项目的开发者省去从零配置的繁琐三是刚接触 tRPC 和 Prisma想看看这些工具在实际项目里怎么配合使用的朋友。1.2 为什么选 T3 Stack 而不是传统前后端分离很多人会问你做一个全栈项目为什么不用 React Vite 写前端、Node Express 写后端非要搞一个 T3 Stack我的回答很简单T3 Stack 的核心价值不在技术新而在类型统一。传统前后端分离模式下数据流是这样的数据库表结构 - ORM 模型 - 后端接口 - 接口文档 - 前端请求封装 - 前端类型定义。中间每一步都可能出现信息损耗最典型的就是后端改了字段名接口文档忘了同步前端调用时直接 undefined。你排查半天最后发现是文档和代码不一致的问题那种挫败感做过联调的人都懂。而 T3 Stack 的链路是数据库表结构 - Prisma schema 定义类型 - tRPC router 暴露 procedure - 前端直接 import 类型调用。Prisma 生成的类型通过 tRPC 的inferRouterInput/inferRouterOutput工具类型暴露给前端中间没有任何手写的接口类型层。改一个字段从数据库到前端提示一气呵成编译期就告诉你哪里不匹配。选 Next.js 作为框架层也有讲究。Next.js 的 App Router 支持服务端组件RSC可以在服务端直接调用 tRPC procedure 查询数据然后把渲染好的 HTML 返回既保留了类型安全又拿到了 SSR 的 SEO 优势和首屏性能。这是纯 SPA 方案很难同时做到的两点。1.3 项目定位与功能边界t3code 不是一个大而全的业务系统它更接近一个精心配置过的起点。我把边界划得很清楚用户注册、登录、会话管理NextAuth.js Credentials GitHub OAuth一个完整的 CRUD 示例文章或任务管理演示 tRPC 的 query / mutation 写法基于 Prisma 的数据库建模和迁移流程Tailwind 定制的响应式布局和主题体系部署相关的环境变量配置和 Docker 示例这个边界帮我避免了一个常见陷阱——脚手架项目做得太满业务代码一大坨反而让人看不出核心架构。你拿到 t3code 后把示例业务删掉替换成自己的模型和 router就能立刻开工。2. 核心技术点逐项拆解2.1 Next.js App Router项目的地基t3code 选择了 Next.js 14 的 App Router 模式。和旧的 Pages Router 相比App Router 的app/目录结构更强调文件系统路由 布局嵌套。在你刚开始接触这套结构时有件事必须先在脑子里建立起来layout.tsx 和 page.tsx 的分工。layout.tsx 定义的是跨页面共享的 UI 框架比如 nav 栏、侧边栏、全局 Provider 包裹page.tsx 定义的是具体路由的内容。layout 会嵌套app/layout.tsx是全局根布局app/(auth)/layout.tsx可以单独给认证相关页面套一层样式app/dashboard/layout.tsx又能给控制台页签式导航。这种嵌套在设计页面骨架时非常灵活代价是层级一多新手容易搞混哪个 layout 包裹了哪个 page。我的建议是画一张静态的路由-布局嵌套图示意图就行头脑里始终有布局树这个概念。t3code 里我用了 Route Group就是目录名带括号的(auth)、(main)这种形式来组织页面Route Group 不会影响 URL 路径纯粹是代码组织层面的分组。这是个非常实用的技巧比如app/(auth)/login/page.tsx的 URL 就是/login括号里的(auth)只是在文件系统层面帮你归类。2.2 tRPC类型安全的 API 层是怎么工作的tRPC 是 T3 Stack 的灵魂组件。很多第一次接触 tRPC 的人会困惑它看起来就是个 RPC 框架和 GraphQL 有什么区别和 REST 相比优势在哪说人话版解释tRPC 让你像调用本地函数一样调用远端接口。你在服务端定义// server/api/routers/post.ts import { z } from zod; import { createTRPCRouter, publicProcedure, protectedProcedure } from ~/server/api/trpc; export const postRouter createTRPCRouter({ getLatest: publicProcedure.query(async ({ ctx }) { const posts await ctx.db.post.findMany({ orderBy: { createdAt: desc }, take: 5, }); return posts; }), create: protectedProcedure .input( z.object({ title: z.string().min(1), content: z.string().min(10), }) ) .mutation(async ({ ctx, input }) { return ctx.db.post.create({ data: { title: input.title, content: input.content, authorId: ctx.session.user.id, }, }); }), });然后在客户端调用// app/dashboard/page.tsx客户端组件 use client; import { api } from ~/trpc/react; export default function DashboardPage() { const { data: posts, isLoading } api.post.getLatest.useQuery(); const createPost api.post.create.useMutation({ onSuccess: () { // 触发重新拉取 utils.post.getLatest.invalidate(); }, }); // ... }注意几个关键点api.post.getLatest.useQuery()里的getLatest方法名、入参类型、返回值类型全部是从服务端 router 类型推导出来的。你写错了方法名或者传错了参数类型编译期直接报错运行期根本走不到那一步。这就是 tRPC 的核心优势它没有 GraphQL 那套 schema 定义和解析层也没有 REST 那套 URL 设计规范和序列化约定它就是 TypeScript 的类型系统在客户端和服务端之间直接传导。2.3 Prisma数据库建模与类型生成的正确姿势t3code 的数据层用的是 Prisma ORM。Prisma 的核心文件是schema.prisma它用一种声明式的 DSL 定义数据模型// prisma/schema.prisma generator client { provider prisma-client-js } datasource db { provider postgresql url env(DATABASE_URL) } model User { id String id default(cuid()) name String? email String unique emailVerified DateTime? image String? passwordHash String? posts Post[] accounts Account[] sessions Session[] createdAt DateTime default(now()) updatedAt DateTime updatedAt } model Post { id String id default(cuid()) title String content String author User relation(fields: [authorId], references: [id]) authorId String createdAt DateTime default(now()) updatedAt DateTime updatedAt }这里有三个实操要点第一所有模型的主键都用cuid()而不是自增 ID。原因很简单t3code 后续可能要做分库分表或分布式部署自增 ID 在分布式场景下会产生冲突而 cuid 是客户端生成的、无序的、几乎不可能碰撞的字符串。另一个实际好处是在 tRPC 里做乐观更新时你可以在数据库返回之前就拿到新记录的 ID用户体验更流畅。第二模型间关系一定要在两端都声明。上面User有posts Post[]Post有author User relation(...)Prisma 会通过authorId外键自动关联。如果只写一端跑prisma generate时会报错提示补全关系字段这个错误很常见看到别慌。第三每次修改 schema 后要跑两条命令npx prisma migrate dev生成迁移文件并应用到开发库和npx prisma generate重新生成客户端类型。很多时候你改了 schema 但忘了 re-generate前端 TypeScript 提示还是旧类型会被这个坑绊一下。2.4 NextAuth.js认证流程与会话处理t3code 里集成了 NextAuth.jsv4同时配置了 Credentials 登录邮箱密码和 GitHub OAuth。Credentials 的配置里最绕的一个点是自定义 authorize 函数。你不能直接拿数据库里的密码做比对必须用 bcrypt 之类工具先哈希存储登录时再哈希比对// server/auth.ts 片段 import Credentials from next-auth/providers/credentials; import bcrypt from bcryptjs; import { db } from ~/server/db; export const authOptions { providers: [ Credentials({ credentials: { email: { label: 邮箱, type: email }, password: { label: 密码, type: password }, }, async authorize(credentials) { const user await db.user.findUnique({ where: { email: credentials.email }, }); if (!user?.passwordHash) return null; const valid await bcrypt.compare(credentials.password, user.passwordHash); if (!valid) return null; return { id: user.id, email: user.email, name: user.name }; }, }), // GitHub Provider 需要环境变量 GITHUB_ID 和 GITHUB_SECRET ], session: { strategy: jwt }, callbacks: { jwt({ token, user }) { if (user) token.id user.id; return token; }, session({ session, token }) { if (token.id) session.user.id token.id; return session; }, }, };这里有个TypeScript 类型扩展的问题默认的Session[user]只有name、email、image你要在 session 里拿user.id必须自己在types/next-auth.d.ts里做模块声明扩展否则session.user.id会报类型错误。// types/next-auth.d.ts import { DefaultSession } from next-auth; declare module next-auth { interface Session { user: { id: string; } DefaultSession[user]; } }不写这个扩展文件你的 protectedProcedure 里拿ctx.session.user.id就会报错。这个坑几乎所有玩 T3 Stack 的人都踩过建议一开始就写好。2.5 Tailwind CSS原子化样式的正确打开方式样式方案选了 Tailwind CSS这个选择有人喜欢有人骂但从效率角度说在 t3code 这种以快速搭建原型和中小型业务系统为目标的脚手架里Tailwind 确实是性价比最高的选择。用 Tailwind 最容易犯的错误是把所有东西都堆在 className 里写了一行几十个类名可读性很差。我实践下来有几条原则组件化是必须的把重复出现的样式模式抽成 React 组件或容器组件别让 JSX 里全是 className 长串善用apply在 CSS 文件里将高频样式组合提取成语义化类名比如.card { apply rounded-lg border border-gray-200 p-6 shadow-sm; }用 design token 统一视觉在tailwind.config.ts里定义colors.primary、colors.accent这类语义色板业务代码里不直接写bg-blue-500而是写bg-primary方便后期换主题Tailwind 还有一个隐性好处是减少了 CSS 文件的体积它只编译你实际用到的类。但副作用是如果你的某个类名是动态拼接的比如bg-${color}-500Tailwind 编译时抓不到样式就不生效。这是个常见坑解决方案是用完整的类名字符串映射表不要动态拼类名。3. 实操过程与核心环节实现3.1 环境准备与项目初始化t3code 我的初始化方式是直接用官方脚手架# 使用 create-t3-app 初始化项目 npx create-t3-applatest t3code # 交互式选择模块 # ✔ Include Tailwind CSS? yes # ✔ Include tRPC? yes # ✔ Include Prisma? yes # ✔ Include NextAuth.js? yes # ✔ Include Drizzle? no初始化完成后目录结构是这样的t3code/ ├── prisma/ │ └── schema.prisma ├── public/ ├── src/ │ ├── app/ │ │ ├── (auth)/ │ │ │ ├── login/ │ │ │ └── register/ │ │ ├── (main)/ │ │ │ ├── dashboard/ │ │ │ └── posts/ │ │ ├── api/ │ │ │ └── auth/[...nextauth]/ │ │ ├── layout.tsx │ │ └── page.tsx │ ├── server/ │ │ ├── api/ │ │ │ ├── root.ts │ │ │ ├── routers/ │ │ │ └── trpc.ts │ │ ├── auth.ts │ │ └── db.ts │ ├── trpc/ │ │ ├── react.tsx │ │ └── server.ts │ └── styles/ │ └── globals.css ├── .env ├── tailwind.config.ts ├── tsconfig.json └── package.json这个结构看起来很规整但有一个小缺点src/app/(auth)/login/page.tsx这种 Route Group 嵌套在 App Router 里有时候会让新人发懵。我建议在 README 里加一张直观的路由表格目录路径URL 路径说明app/page.tsx/首页app/(auth)/login/page.tsx/login登录页app/(auth)/register/page.tsx/register注册页app/(main)/dashboard/page.tsx/dashboard控制台app/(main)/posts/page.tsx/posts文章列表3.2 环境变量的配置与保护环境变量这块T3 Stack 默认使用t3-oss/env-nextjs做运行时的环境变量校验这个设计我特别推荐。你不光是把DATABASE_URL、NEXTAUTH_SECRET写进.env还要在src/env.mjs里声明它们的类型和必填约束// src/env.mjs import { createEnv } from t3-oss/env-nextjs; import { z } from zod; export const env createEnv({ server: { DATABASE_URL: z.string().url(), NEXTAUTH_SECRET: z.string().min(1), GITHUB_ID: z.string().optional(), GITHUB_SECRET: z.string().optional(), NODE_ENV: z.enum([development, test, production]), }, client: { NEXT_PUBLIC_API_BASE_URL: z.string().url().optional(), }, runtimeEnv: { DATABASE_URL: process.env.DATABASE_URL, NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET, GITHUB_ID: process.env.GITHUB_ID, GITHUB_SECRET: process.env.GITHUB_SECRET, NODE_ENV: process.env.NODE_ENV, NEXT_PUBLIC_API_BASE_URL: process.env.NEXT_PUBLIC_API_BASE_URL, }, });这样做最大的好处是应用在启动阶段就会校验环境变量如果漏配了某个关键的 server 变量直接明晃晃地报错而不是等运行时出现诡异的 500 错误后你再从日志里慢慢排查。这个体验和普通 Node 项目的process.env裸奔写法比是质的提升。一个必须强调的事情.env文件绝对不要提交到 Git 仓库t3code 里我已经把.env.example作为模板提交团队其他人 clone 后复制一份改成自己的配置即可。3.3 从零实现一个带分页的文章列表为了演示 tRPC 加 Prisma 的完整配合我在 t3code 里实现了一个文章列表功能带着分页和关键字搜索。这里我说一下核心代码和思路先定义 Prisma 模型这个我们在 2.3 已经写过。接着写 tRPC router// src/server/api/routers/post.ts import { z } from zod; import { createTRPCRouter, protectedProcedure } from ~/server/api/trpc; const postQuerySchema z.object({ page: z.number().int().min(1).default(1), pageSize: z.number().int().min(1).max(50).default(10), keyword: z.string().trim().optional(), }); export const postRouter createTRPCRouter({ list: protectedProcedure .input(postQuerySchema) .query(async ({ ctx, input }) { const where input.keyword ? { OR: [ { title: { contains: input.keyword, mode: insensitive } }, { content: { contains: input.keyword, mode: insensitive } }, ], } : {}; const [rows, total] await Promise.all([ ctx.db.post.findMany({ where, orderBy: { createdAt: desc }, skip: (input.page - 1) * input.pageSize, take: input.pageSize, include: { author: { select: { name: true } } }, }), ctx.db.post.count({ where }), ]); return { rows, total, page: input.page, pageSize: input.pageSize, totalPages: Math.ceil(total / input.pageSize), }; }), });这段代码里有两个值得说的细节第一Promise.all并发执行查列表和查总数避免了顺序执行造成的额外耗时——这个优化在数据量大时尤其明显两个数据库查询同时发出整体延迟近似单个查询。第二分页参数用 zod 做了运行时校验。page默认 1pageSize最大 50防止有人故意传一个pageSize99999来拖垮数据库。zod 的校验发生在 tRPC 的 input 解析阶段所以非法入参根本不会进入业务逻辑。然后在前端调用时配合useQuery的分页状态// app/(main)/posts/page.tsx客户端组件片段 use client; import { useState } from react; import { api } from ~/trpc/react; export default function PostsPage() { const [page, setPage] useState(1); const [pageSize, setPageSize] useState(10); const [keyword, setKeyword] useState(); const { data, isLoading } api.post.list.useQuery({ page, pageSize, keyword, }); // 渲染文章列表 分页控件 }注意每次点分页按钮、修改搜索框内容useQuery都会自动触发重新请求配合 tRPC 客户端内置的 React Query 缓存上一页的数据不会马上清空体验上几乎没有加载中白屏的顿挫感。3.4 服务端组件直接调 tRPCApp Router 场景下很多页面其实不需要客户端交互纯展示型内容直接在服务端渲染就行。这时候应该用服务端组件调用 tRPC而不是把页面标记为use client。t3code 里的写法是// app/(main)/stats/page.tsx服务端组件注意没有 use client import { api } from ~/trpc/server; export default async function StatsPage() { const stats await api.post.getStats(); return ( div h1文章统计/h1 p总文章数{stats.totalPosts}/p p总评论数{stats.totalComments}/p p最近一周新增{stats.weeklyNew}/p /div ); }关键在于~/trpc/server.ts里导出的api是一个支持在服务端直接调用的 tRPC caller它不走 HTTP、不经过useQuery那套 React Query 逻辑而是直接调用 router 里的 procedure。这样做的好处是首屏 HTML 里就直接包含渲染好的数据不需要客户端再发一次请求拿数据SEO 友好首屏速度也快。这里有一个容易被坑的点~/trpc/server.ts必须在服务端模块里使用如果你在一个客户端组件use client里 import 它会报 Web server is not available 之类的错误。区分组件的服务端/客户端边界是 App Router 开发的核心能力我的经验是默认写成服务端组件只有确实需要交互响应useState、onClick、useEffect 这类时才加use client标记。3.5 注册登录全流程补全t3code 里我用的是一个通用方案注册时创建用户并 bcrypt 哈希密码随后直接调用 NextAuth 的signIn(credentials)完成自动登录。注册的 router// src/server/api/routers/auth.ts import { z } from zod; import bcrypt from bcryptjs; import { createTRPCRouter, publicProcedure } from ~/server/api/trpc; import { TRPCError } from trpc/server; export const authRouter createTRPCRouter({ register: publicProcedure .input( z.object({ name: z.string().min(2, 昵称至少2个字符), email: z.string().email(邮箱格式不正确), password: z.string().min(8, 密码至少8位), }) ) .mutation(async ({ ctx, input }) { // 检查邮箱是否已注册 const existing await ctx.db.user.findUnique({ where: { email: input.email }, }); if (existing) { throw new TRPCError({ code: CONFLICT, message: 该邮箱已被注册, }); } const passwordHash await bcrypt.hash(input.password, 10); await ctx.db.user.create({ data: { name: input.name, email: input.email, passwordHash, }, }); return { ok: true }; }), });这里有两个实操细节**bcrypt 的 cost factor10**是我经过测试后选择的折中值。cost 太低比如 4、5哈希速度很快但安全性差容易被暴力破解cost 太高比如 14每次登录都要等一秒多用户体验糟糕。在我测试的机器上cost10 单次哈希大约 50~80ms在安全性和响应速度之间比较均衡。错误码语义化。TRPCError的code字段有多种取值CONFLICT表示资源冲突客户端可以据此区分参数错误和业务冲突从而给出不同的错误提示。不要在 message 里堆砌太多信息更不要把数据库原始错误直接抛给前端。注册成功后前端可以这样收尾const register api.auth.register.useMutation({ onSuccess: async () { await signIn(credentials, { email: form.email, password: form.password, redirect: true, callbackUrl: /dashboard, }); }, onError: (err) { toast.error(err.message); }, });3.6 部署实践的要点记录t3code 我最后部署到了 Linux VPS 上用的方式是Next.js 的 standalone 输出模式 PM2 Nginx 反代 PostgreSQL。先说 standalone 模式。在next.config.mjs里加一句output: standalone构建产物里就会生成一个独立的 node_modules 目录和 server.js 入口文件体积比整体 node_modules 小很多也更适合容器化部署。Dockerfile 的核心思路是多阶段构建# 阶段一依赖安装与构建 FROM node:20-alpine AS builder WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN corepack enable pnpm install --frozen-lockfile COPY . . RUN pnpm run db:generate RUN pnpm build # 阶段二运行 FROM node:20-alpine AS runner WORKDIR /app ENV NODE_ENVproduction COPY --frombuilder /app/.next/standalone ./ COPY --frombuilder /app/.next/static ./.next/static COPY --frombuilder /app/public ./public EXPOSE 3000 CMD [node, server.js]构建阶段的db:generate是prisma generate的别名这一步很重要——构建镜像时如果不生成 Prisma Client运行时ctx.db.post.findMany会直接报错说 client 未初始化。Nginx 反代配置里要特别注意 proxy 头的设置和上传体积限制不过 t3code 目前还没有文件上传功能所以默认client_max_body_size1M 就够了。等后面接了对象存储做图片上传再单独调这个值。4. 常见问题与排查技巧实录4.1 Prisma Client 类型未更新现象改了schema.prisma里的模型字段但前端调用ctx.db.post.findMany时TypeScript 智能提示还是老字段。原因prisma generate没有执行或者执行后 TypeScript 语言服务还没刷新。解决方案按顺序执行npx prisma migrate dev --name your_migration和npx prisma generate然后在 VS Code 里CtrlShiftP打开命令面板搜索TypeScript: Restart TS Server重启语言服务。我在 t3code 的 package.json 里把这两个命令合并成了db:push和db:generate两个脚本日常开发流程就是pnpm db:push pnpm db:generate。4.2 NextAuth 回调地址不在白名单现象GitHub OAuth 登录时GitHub 页面报 The redirect uri ... is not authorized for this application。原因GitHub OAuth App 的 Authorization callback URL 和你本地/线上的回调地址不一致。NextAuth 的回调默认是/api/auth/callback/github。解决方案去 GitHub Settings - Developer settings - OAuth Apps 里把完整回调地址填上。本地开发填http://localhost:3000/api/auth/callback/github线上填https://你的域名/api/auth/callback/github。如果同时在本地和线上调试建议配两个环境变量分别对应。4.3 tRPC 客户端请求一直 500现象前端所有useQuery都返回 500服务端日志也看不到明显异常。排查路径先用 curl 直接打服务端 REST 端点确认服务是否正常再检查 tRPC endpoint 是否挂在/api/trpc下面。T3 Stack 默认约定是src/app/api/trpc/[trpc]/route.ts如果你手动改过路由或用了自定义前缀客户端创建时也要对应改url字段。我遇到过一次比较隐蔽的问题部署环境漏了NEXTAUTH_SECRET导致 NextAuth 初始化失败间接导致 tRPC 的createContext里访问session时报错所有带鉴权的 procedure 全部 500。环境变量校验器在启动时没拦住因为那台机器上.env文件里的变量名拼错了。解决后我专门写了个脚本在部署前做 env 完整性自检。4.4 服务端组件误用 useState现象在 page.tsx 里写了useState构建时报 Youre importing a component that needs useState. It only works in a Client Component but none of its parents are marked with use client。原因App Router 默认所有组件都是服务端组件服务端组件不支持使用 hooks。解决方案在文件顶部加use client标记或者把交互逻辑抽到单独的子组件里标记use clientpage 本身保持服务端渲染。我在 t3code 里用了一个约定凡是带表单、按钮、分页控件、状态管理的组件一律命名为*.client.tsx之类的后缀这样文件结构里一眼能看出边界不容易混。4.5 Tailwind 动态类名不生效现象写了className{bg-${color}-500}但页面上没有对应背景色。原因Tailwind JIT 编译器是静态扫描源码的动态拼接的类名在编译阶段无法确认所以不会生成对应 CSS。解决方案改用完整的映射表const colorClassMap { blue: bg-blue-500, red: bg-red-500, green: bg-green-500, } as const; // 使用 className{colorClassMap[color]}4.6 常见错误速查表错误信息可能原因快速方案PrismaClientInitializationError数据库连接字符串错误或数据库未启动检查.env的DATABASE_URLpnpm db:push验证连通性TS2339: Property id does not exist on type Session没写 types/next-auth.d.ts 扩展补上模块声明并重启 TS ServerTRPCClientError: http 500环境变量缺失或 procedure 内抛错查看服务端日志确认 env 完整性Error: Cannot find module prisma/client容器构建时没执行prisma generate在构建阶段执行pnpm db:generateHydration failed because UI does not match客户端/服务端渲染内容不一致避免在渲染时依赖随机值或客户端时间确保数据源稳定5. 从 t3code 延伸这套架构还能怎么玩t3code 完成基础版本之后我陆陆续续在它上面加了一些进阶实验我觉得这些方向很有价值值得在这里分享一下。第一个是文件上传。Prisma 适合存文件的元数据文件名、URL、大小、mimeType真实的二进制文件我放到本地磁盘或对象存储里。流程是客户端拿到文件后先通过 tRPC mutation 申请一个上传凭证返回上传 URL 或 uploadId然后直接通过 HTTP PUT 上传最后再用 tRPC mutation 确认上传完成并落库。这样避免了把大文件二进制通过 tRPC 的 JSON 序列化走一遍性能上好很多。第二个是Webhook 回调。t3code 做支付回调时需要处理一个安全问题第三方平台 POST 过来的签名验证。tRPC 的 publicProcedure 天然支持这种无鉴权的端点我只需要在 procedure 里用 zod 校验 body 结构、验证请求签名即可。注意要给这个 router 加上限流防止恶意刷请求。第三个是软删除和审计日志。给核心模型加deletedAt字段所有删除操作走update而不是delete再配合一个AuditLog模型记录操作人和操作内容。这个对小型团队做权限追溯很有帮助而且实现成本很低只要在 tRPC router 里养成先查后改的习惯就行。第四个是一个我最近在玩的方向React Server Components 结合 tRPC 的流式渲染。当列表数据特别大时可以用Suspense包裹异步组件让页面先渲染骨架屏数据到了再填充。这套模式和 t3code 现有的服务端组件思路自然衔接体验提升明显。6. 一些掏心窝的操作心得6.1 关于依赖版本别急着升级t3code 用的这套技术栈迭代速度很快尤其是 Next.js、tRPC、Prisma 这三个几乎每半年就有一个大版本。我见过不少项目因为盲目升级依赖API 变更导致大量代码要重写。我的态度是项目运行稳定就别轻易动大版本跟着 create-t3-app 的模板走等社区沉淀出新项目的标准做法再评估升级路径。6.2 写清楚你的为什么t3code 的代码里我加了不少注释但不是解释这段代码干什么而是解释为什么这么写。比如环境变量校验那段我会写下之所以用 zod 做运行时校验是因为如果漏配变量希望在启动时快速失败而不是运行时出现诡异的 500。这种注释对未来的自己或者接手的同事价值最大。6.3 用种子数据加速开发我写了一个prisma/seed.ts脚本里面生成了几十条带不同标签、不同作者的假文章。每次重置数据库后跑一遍pnpm db:seed开发环境就有真实可用的数据不用手动去后台一条条录。这个习惯对调试分页、搜索、排序功能尤其重要没有种子数据你很难在页面上直观地验证分页逻辑是否正确。6.4 关于前后端分离的执念最后说点直接的。我之前是坚定的前后端分离主义者觉得前端就该独立部署后端就该做 API 服务谁也别干涉谁。但 T3 Stack 的实践让我看到一个更务实的选项对于中小型项目、内部工具、个人项目全栈一体化的开发效率是分离架构比不了的。你不写接口文档、不维护 API 版本、不担心 CORS、不需要单独的 mock 服务类型安全贯穿全链路很多联调的痛苦直接消失。当然如果你的团队有严格的并发开发需求——5 个前端、5 个后端同时在同一个代码库上开发那 T3 Stack 未必是最优解前后端分离反而更利于边界管理。架构选型从来没有银弹只有适不适合当前的团队规模和项目阶段。t3code 的选择是基于一个人或小团队要快速做出可用产品这个前提的它帮你省掉的不是写代码的时间而是大量联调、对类型、排查文档不一致的无意义时间。t3code 还在持续迭代我目测接下来会加两个东西一是把内置的 Tailwind 组件库整理成一套可复用的 UI 基础组件二是支持多租户的权限模型。如果你正在纠结要不要从传统前后端分离转向全栈类型安全路线或者只是想找一个可以直接跑起来的脚手架参照t3code 应该能给你不少启发。自己动手把流程完整走一遍比看十篇教程都管用。

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

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

免费获取报价 →
↑