尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
t3code:用T3 Stack构建端到端类型安全的全栈开发模板
1. 为什么这个项目叫 t3code一个类型安全强迫症的自白1.1 项目缘起前后端割裂带来的痛苦t3code 这个名字最早其实是我本地一个文件夹的名字。当时项目名还没想好随手敲了t3code作为脚手架目录名准备跑通 T3 Stack 之后再看情况改名。结果这个项目一路写下来从 demo 变成了模板又从模板变成了我几乎所有新项目的起点名字也就一直沿用下来了。所谓 T3 Stack指的是把 Next.js、TypeScript、Tailwind CSS、tRPC 这几样东西组合起来再辅以 Prisma 和 NextAuth形成一套端到端类型安全的全栈开发方案而 t3code就是我在这套方案上沉淀出的具体落地形态。之所以会做这件事是因为我过去很长一段时间都在被前后端割裂这件事恶心。传统的开发模式很简单后端写 REST API用 Swagger 维护接口文档前端自己去调接口然后在本地维护一份接口对应的 TypeScript 类型。听起来可行但你只要经历过一次后端改了字段名前端完全不知道直到运行时才报错的尴尬就会明白问题有多严重。我们不是没努力过有人约定先写类型定义有人用 codegen 自动生成接口类型但这些方案都引入了额外的概念负担和构建环节。我更想要的效果是类型定义只写一份前后端自动共享改错了一个字段编译期就大声告诉你。t3code 的核心目标就是解决这个痛点。它继承了 T3 Stack 的理念——用尽量少的工具换来尽量多的端到端类型安全。简单说从数据库到 API 再到前端组件整条数据链路都能保持类型一致中间不需要手写任何桥接代码。这篇文章我会从选型逻辑、搭建过程、核心机制、实战功能、踩坑记录一直讲到部署思路基本覆盖我在这类项目里能遇到的所有环节。如果你也打算尝试全栈类型安全开发或者已经在用 T3 Stack 但想少走弯路这份记录应该值得你花点时间。1.2 t3code 能做什么从脚手架到业务模板的定位先给 t3code 一个准确的定义。它不是一个框架也不是一个新的轮子而是一套基于 T3 Stack 的全栈项目骨架顺带包含了一个带认证的用户管理模块作为参考实现。你可以把它理解为开箱即用的类型安全全栈起手式克隆下来之后数据库模型、API 层、认证流程、前端页面骨架都已经帮你排布好了你要做的事情是往里面填业务字段。这样的定位其实来自我个人的使用习惯。我经常要开新项目每次开新项目最烦的不是业务逻辑而是重复的环境配置初始化 Next.js、配 Tailwind、装 tRPC、连接数据库、折腾 NextAuth、写一个 demo 页面验证链路通不通。这些工作一旦熟练之后你会发现它们毫无技术含量纯粹是体力活。把 t3code 维护成一套模板之后我新项目的初始化时间从一两个小时压缩到了十几分钟而且所有配置都是经过反复验证的比每次临时拼装靠谱得多。对于刚接触 T3 Stack 的读者这份模板也是一个很好的学习样本你可以通过阅读它的目录结构、路由定义、鉴权流程看到整套技术栈是如何协作的。我在后面几章里讲述的所有内容都是基于这个模板展开的它不是理论推演而是我自己在真实项目中反复改出来的结果。2. 技术选型的底层逻辑T3 Stack 每一环为什么不可替代2.1 Next.js App Router TypeScript骨架和肌肉很多人在选型时会问为什么不是 Vite React SPA为什么一定要 Next.js我的答案是因为 t3code 这类项目天然需要 BFFBackend for Frontend角色。你当然可以用纯前端项目再加一个 Node 服务端但那样你就得自己处理跨域、鉴权、布署两台服务的问题。Next.js 把前端页面、服务端渲染、API 路由放在同一个应用里开发和部署的心智负担都小很多这是它最核心的价值。App Router 更是把这种能力往前推了一大步。Server Component 可以直接在服务端读数据库、调 tRPC 函数甚至不需要向前端暴露接口。Layout 嵌套机制也特别适合处理全站都要显示导航栏后台管理区域需要独立鉴权这类需求。你只需要在某个子目录下放一个layout.tsx它就能包住下面所有页面。这套机制在 t3code 里是骨架级别的存在页面怎么组织、权限在哪一层拦截全都靠它。TypeScript 则是整套方案的肌肉。注意我说的是严格模式下的 TypeScript不是那种哪哪都是any的 TypeScript。t3code 里我开了strict: true配合noUncheckedIndexedAccess这类强化选项让编译器帮你挡住空指针和类型误用。有人觉得这样写代码很啰嗦但我的经验是在全栈项目里类型系统的严谨程度直接决定你重构时的底气。后面讲 tRPC 的部分你会看到TypeScript 的类型推断能力是整个端到端类型安全的基础没有它tRPC 的威力直接减半。2.2 Tailwind CSS不用组件库是故意的t3code 的前端样式用的 Tailwind CSS而不是某个现成的组件库。这不是因为组件库不好而是因为 t3code 的定位是模板不是业务项目。组件库都有自己的设计语言比如 Material Design你要是用它做模板将来业务页面难免会被组件库的样式惯性带着走。Tailwind 提供的是 utilities不是组件它只约束你怎么快速写出样式不强迫你长成什么样子自由度更高也更容易改成你自己项目的风格。另一个选 Tailwind 的理由是开发体验。写样式和写逻辑在同一文件里不用来回切 CSS 文件也不用想类名——这个痛点写过 CSS 的人都有体会。配上clsx和tailwind-merge之后条件类名的拼接也变得干净多了。我在 t3code 里做了一个小组件来封装按钮的变体逻辑用的就是这两个工具既避免了类名覆盖问题也让按钮不同状态的切换变得可读。对于模板类项目这种轻依赖、重约定的选型思路我觉得更健康。2.3 tRPC 出现之前我们都在做类型翻译提到 tRPC我得先讲一下它解决的真实痛点。在传统 REST 架构里后端有一个返回值类型前端有一个自己维护的同名类型。名字一样但两处定义互不相通这就是所谓的类型翻译。前端改了个字段名后端不知道后端改了类型前端也可能漏同步。只要你经历过一次线上环境因为这种低级错误导致的白屏你就会意识到HTTP 边界上的类型安全比进程内部的类型安全重要得多。tRPC 的做法是直接取消掉两处定义。你的 API 不是一组 URL而是 TypeScript 里的普通函数。后端定义procedure前端直接按调用函数的方式去使用它。关键在于这个函数的参数类型和返回值类型是从服务端代码里自动推导出来的前端不需要写任何额外的类型定义。也就是说后端加一个字段、改一个参数前端代码里对应的类型立刻变化编译器马上就能告诉你哪里需要跟着改。有人会拿 GraphQL 来跟 tRPC 比。GraphQL 也有类型系统但它的类型定义是 schema 层面的仍然需要前后端同时维护或由 codegen 生成。tRPC 则干脆得多——它把类型源头放在服务端函数本身你写出来的代码就是类型。我更倾向 tRPC 的原因很简单少一层抽象少一堆概念TypeScript 全栈项目里已经够复杂的了没必要再引入一个 schema 语言。2.4 Prisma NextAuth数据和身份的守门员数据层我选了 Prisma认证选了 NextAuth现在叫 Auth.js。前者负责我有哪些数据、长什么样后者负责谁在访问这些数据两者都是 tRPC 的强依赖。Prisma 的价值在于 schema 即文档数据模型定义清晰而且它生成的客户端自带完整的 TypeScript 类型数据库查询结果是什么类型返回值就是什么类型不会出现手写 SQL 映射类型时的那种断层。NextAuth 则是目前集成度最高、最省心的 Next.js 认证方案。它支持 GitHub、Google、邮箱密码等多种登录方式还提供了getServerSession这样的服务端方法方便我在 tRPC 的 Context 里取到当前登录用户。更关键的是NextAuth 的 session 类型可以定制——我可以在 t3code 里把session.user.id直接关联到数据库里的 User 主键这样后面所有受保护的操作都不需要再查一遍用户表。还有一层容易被忽略的是数据校验。t3code 沿用了 create-t3-app 的惯例核心的 tRPC procedure 都会用 Zod 对输入做校验。Zod 配合 tRPC 有一个特别舒服的体验输入校验规则写在服务端校验失败的错误信息会原样传回前端前端拿到的错误对象还自带类型。你不需要在前后端各写一遍表单校验逻辑这是很多传统项目做不到的。3. 从 create-t3-app 到 t3code完整的搭建与定制过程3.1 初始化命令与交互选项搭建 t3code 的第一条命令很直接npm create t3-applatest t3codecreate-t3-app会问你几个交互式问题用不用 Next.js 的 App Router要不要 Tailwind CSS要不要 tRPC要不要 Prisma要不要 NextAuth以及使用什么包管理器。我当时的选项是App Router 开启、Tailwind 开启、tRPC 开启、Prisma 开启、NextAuth 开启、包管理器用 pnpm。也可以调用 CLI 参数跳过交互适合脚本化创建项目。这一步的产出不是一个空项目而是一个已经跑通的、包含了基础页面和示例路由的 T3 Stack 工程。它的核心目录结构大致如下src/ app/ # 页面路由App Router components/ # 公共组件 server/ # 服务端逻辑 api/ # tRPC 路由定义 db.ts # Prisma 单例 auth.ts # NextAuth 配置 trpc/ # tRPC 客户端连接逻辑 styles/ prisma/ schema.prisma # 数据模型值得一提的是src/trpc/这个目录。create-t3-app 会在这里生成react.tsx和server.ts两个文件它们分别负责两件事react.tsx构建了供前端组件消费的 tRPC React 客户端server.ts则构建了服务端侧的转发器方便在 Server Component 里直接调用 API。理解这两个文件的区别是理解 T3 Stack 的关键之一。3.2 目录结构与业务模块划分很多人初始化完脚手架就直接开写结果过了两周发现目录一片混乱。我在 t3code 里维持了几条基本原则页面文件只放页面结构业务组件放componentsAPI 逻辑全部收敛到server/api/routers。这样做的原因是 tRPC 的 router 是树状结构它天然要求你把业务模块拆分干净而不是把所有 procedure 塞进一个文件里。以 t3code 的用户管理模块为例我在routers/下面建了user.ts和post.ts两个文件然后在root.ts里把它们合并成一个appRouter。这么做的好处不只是代码看起来整洁更重要的是类型文件的粒度。tRPC 的客户端类型推断会加载整个 router 树如果你的根 router 里塞了几百个 procedure前端每次编译都要做大量类型计算页面复杂后明显变慢。按业务模块拆分 router等于把类型计算的范围也切小了。对外暴露的类型只有AppRouter一个前端所有调用都从它身上取类型。这是一个很有价值的心智模型不管后端拆了多少个文件前端视角里的 API 入口始终是同一个类型对象你不需要知道后端具体文件分布就能获得完整的类型提示。3.3 环境变量与配置文件容易翻车的前 10 分钟初始化完成后第一个坑往往出现在环境变量上。T3 Stack 有三类变量非常关键DATABASE_URLPrisma 连接字符串、NEXTAUTH_SECRET会话加密密钥、NEXTAUTH_URL部署环境下的站点地址。如果你用的是 GitHub 登录加一个AUTH_GITHUB_ID和AUTH_GITHUB_SECRET用其他 OAuth 服务类似。我的建议是立刻创建.env文件并同时维护一个.env.example提交到仓库里把变量名和注释都写好方便其他协作者一眼看懂该填什么。NEXTAUTH_SECRET可以用openssl rand -base64 32生成不要自己手打也别用固定的弱口令。还有一个特别容易被忽略的点如果本地和线上用同一个 Prisma schema但数据库一个用 PostgreSQL 一个用 SQLiteDATABASE_URL的格式差别会让你迁移时痛苦不已。所以 t3code 的开发环境我用的是 PostgreSQL线上也是 PostgreSQL避免本地和生产行为不一致。配置文件的另一个重点是next.config.js。T3 Stack 默认会接一个tRPC react query相关配置和一些环境变量的注入。如果你后续要加图片域名白名单、自定义 webpack 配置注意先搞清楚哪些是模板自带的别一上来就整体替换掉。4. tRPC 端到端类型安全t3code 的核心魔术是怎么工作的4.1 从 procedure 到 router一次请求的完整旅程tRPC 理解起来其实很简单。它不定义新的传输协议底层还是 HTTP JSON默认你在服务端写一个函数前端像调用本地函数一样调用它仅此而已。但这个仅此而已的背后有几个关键概念需要弄清楚。第一个概念是procedure过程。一个 procedure 是 tRPC API 的最小单元它可以是query查询请求等价于 GET 语义也可以是mutation写操作等价于 POST/PUT/DELETE 语义。一个 child procedure 通常长这样import { z } from zod; import { createTRPCRouter, protectedProcedure } from ~/server/api/trpc; export const postRouter createTRPCRouter({ getRecent: protectedProcedure .input(z.object({ limit: z.number().min(1).max(50).default(10) })) .query(async ({ ctx, input }) { return ctx.db.post.findMany({ orderBy: { createdAt: desc }, take: input.limit, }); }), });这个query函数的第一个参数ctx里已经有当前请求的上下文第二个参数input是经过 Zod 校验之后的输入。整个过程类型完整input.limit一定是number返回值一定是 Prisma 根据findMany推断出的类型。没有手写接口文档没有单独的类型定义文件。第二个概念是router它就是 procedure 的容器可以类比命名空间。你把一组相关的 procedure 放进一个 router再把多个子 router 挂到根 router 上export const appRouter createTRPCRouter({ user: userRouter, post: postRouter, });前端调用时路径就是api.post.getRecent清晰直观。当请求到达服务端时tRPC 会先加载 Context再校验输入再执行对应的 handler。这里的执行顺序是固定的所以后面我们会把鉴权放在 procedure 中间件这一层去做。4.2 Context 和请求级依赖注入Context 是 tRPC 里特别容易被低估的一环。它本质上是每个请求进入时构建好的对象可以放数据库客户端、用户会话、请求 ID 这些东西。t3code 里的 Context 是这样构建的export const createTRPCContext async (opts: { headers: Headers }) { const session await getServerSession(authOptions); return { db, session, ...opts, }; };注意getServerSession是 NextAuth 提供的方法它在服务端拿到当前登录用户的会话信息。这意味着每个 tRPC 请求进来时我们都已经把当前用户是谁放进了ctx后面的代码不用再去重新反序列化 cookie 或 token。这就是依赖注入的思维方式你不在业务代码里手动去获取当前用户而是在请求入口统一注入。有了 Context就可以写受保护的 procedure。T3 Stack 里的protectedProcedure是这样定义的export const protectedProcedure t.procedure.use(({ ctx, next }) { if (!ctx.session?.user) { throw new TRPCError({ code: UNAUTHORIZED }); } return next({ ctx: { session: { ...ctx.session, user: ctx.session.user }, }, }); });只要 procedure 挂在这个中间件后面它天然就拥有了一个非空的ctx.session.user。类型系统会强制这一点——你不需要在 handler 里再判断如果用户为 null 怎么办因为编译期就知道这个值一定是存在的。我第一次用这个模式时的感受是原来鉴权也可以通过类型系统来约束而不只是运行时判断。4.3 客户端调用链TypeScript 是怎么把类型传过去的前端部分t3code 使用了 tRPC 官方基于 React Query 封装的 hooks。初始化逻辑在 create-t3-app 里已经替我们写好了export const api createTRPCReactAppRouter();这行代码是整条链路的起点。它把服务端的AppRouter类型传给了前端从此之后你所有组件的api.post.getRecent.useQuery()都能感知到这个 query 的入参必须符合{ limit?: number }返回值一定是一个包含createdAt、title等字段的数组。如果你在服务端给 query 改了返回值结构前端编译立刻报错不会等到运行时才发现。还有一个容易被忽略的细节tRPC 默认的数据序列化不支持Date、Map、undefined这些值。create-t3-app 默认在实例化里加了superjson这个 Transformer它专门解决跨网络传输时的类型问题。如果你自己手动搭建 tRPC千万别漏掉这一项——否则你在 Prisma 查询里拿到的createdAt一个Date对象传到前端之后会变成字符串所有跟时间相关的逻辑都会出问题。类型传递之所以能做到这一步核心是 TypeScript 条件类型和类型推断的功能。你在前端只声明了AppRouter类型TypeScript 就会递归地解析出所有 router 分支推导出对应 procedure 的输入输出类型。这套机制听着玄乎实际使用体验却非常朴素就像你调用一个同进程的函数参数类型错了编辑器直接给你标红。5. 真实功能落地一个带认证的发布模块5.1 Prisma Schema 设计和迁移写博文不能只讲理论我把 t3code 里一个实实在在的模块拿出来拆一遍。假设我们要做一个带登录的用户发帖功能第一步是设计数据模型。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(false) createdAt DateTime default(now()) updatedAt DateTime updatedAt authorId String author User relation(fields: [authorId], references: [id], onDelete: Cascade) }这里的 User 模型其实和 NextAuth 默认需要的表结构保持一致如果你用 create-t3-app 初始化它默认会带一个包含 Account、Session、User、VerificationToken 的 schema我只需要在 User 上补一个posts关系即可。Post 模型里的authorId是外键onDelete: Cascade表示用户删除时他发的帖子一并删除避免出现孤儿数据。修改完 schema 之后执行迁移npx prisma migrate dev --name add_post_model这个命令会做两件事生成一个新的迁移 SQL 文件并重新生成 Prisma Client。之后你在server/db.ts里的db.post就能直接用了。一个特别实用的经验是在开发阶段迁移文件和代码一起提交你不需要手动去生产环境执行 SQL后面部署部分我会讲怎么自动化处理。我建议为这个模块建一个种子脚本往数据库里插入一个测试用户和两条帖子。否则每次新环境都要手动注册、登录、发帖浪费时间。Prisma 的seed.ts配置在package.json里加一个prisma: { seed: tsx prisma/seed.ts }即可配合npx prisma db seed使用。5.2 书写第一个带鉴权的 postRouter数据模型就绪后我写了server/api/routers/post.ts。它包含三个 procedure查询帖子列表、创建新帖、删除自己的帖子。注意删除自己的帖子这里我刻意用了受保护 procedure并且在 handler 里校验作者身份export const postRouter createTRPCRouter({ list: protectedProcedure .input(z.object({ published: z.boolean().optional() })) .query(async ({ ctx, input }) { return ctx.db.post.findMany({ where: { published: input.published ?? true, }, include: { author: { select: { name: true, image: true } } }, orderBy: { createdAt: desc }, }); }), create: protectedProcedure .input( z.object({ title: z.string().min(3).max(100), 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, }, }); }), delete: protectedProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) { const post await ctx.db.post.findUnique({ where: { id: input.id } }); if (!post) throw new TRPCError({ code: NOT_FOUND }); if (post.authorId ! ctx.session.user.id) { throw new TRPCError({ code: FORBIDDEN }); } return ctx.db.post.delete({ where: { id: input.id } }); }), });这块代码看着不长但包含的知识点不少。第一所有输入都过 Zodtitle太短、content不够长都会在进入数据库之前被拦截。第二create和delete都依赖ctx.session.user.id而这是受保护 procedure 保证非空的。第三delete里做了资源所有权判断防止用户删除别人的帖子。这些校验放在 API 层统一处理前端就不需要重复做太多防御。5.3 React 组件侧调用与表单处理前端页面上t3code 的作者发帖表单是一个很典型的 tRPC React 组合。先看列表组件它被我从服务端页面里切出来是一个use client组件use client; import { api } from ~/trpc/react; export function PostList() { const { data, isLoading, isError } api.post.list.useQuery({ published: true }); if (isLoading) return p加载中.../p; if (isError) return p加载失败/p; return ( ul {data.map((post) ( li key{post.id} h3{post.title}/h3 p{post.content}/p span{post.author?.name}/span /li ))} /ul ); }这里api.post.list.useQuery返回的data类型就是服务端listprocedure 的返回值类型。你需要手动 Annotate 字段吗不需要。TypeScript 已经明白post.id、post.title、post.author?.name是什么类型。这段代码在 App Router 的服务端组件里不能直接用因为 hooks 只能在 client component 里运行——这是很多人第一次用 T3 Stack 时最容易踩的坑我后面在避坑清单里会重点讲。再看创建表单。这里我用了 React 的useState配合api.post.create.useMutation提交成功后调用useRouter().refresh()来刷新 Server Component 数据use client; import { useState } from react; import { useRouter } from next/navigation; import { api } from ~/trpc/react; export function PostForm() { const router useRouter(); const utils api.useUtils(); const [title, setTitle] useState(); const [content, setContent] useState(); const createMutation api.post.create.useMutation({ onSuccess: () { setTitle(); setContent(); utils.post.list.invalidate(); }, }); return ( form onSubmit{(e) { e.preventDefault(); createMutation.mutate({ title, content }); }} input value{title} onChange{(e) setTitle(e.target.value)} placeholder标题 / textarea value{content} onChange{(e) setContent(e.target.value)} placeholder正文 / button typesubmit disabled{createMutation.isPending} {createMutation.isPending ? 发布中... : 发布} /button {createMutation.isError p{createMutation.error.message}/p} /form ); }utils.post.list.invalidate()是 React Query 的失效重取机制它比手动router.refresh()更精确只让列表相关的缓存失效重新请求最新数据而不是刷新整个页面。这也是 tRPC 与 React Query 深度整合带来的好处之一它把服务端状态管理直接内建在了 hooks 里。5.4 错误处理让类型错误和业务错误都在该出现的地方出现错误处理是最能体现 t3code 设计理念的环节。前面说过Zod 校验失败会自动抛一个带ZodError的 tRPC 错误它的 message 会被 JSON 序列化传回前端。这意味着前端拿到的createMutation.error.message已经是经过整理的中文提示了不需要再做额外的二次校验判断。不过有一个经验值得单独说说不要为了省事把所有 tRPC 错误的message直接丢给用户。你内部用的代码、数据库错误信息可能包含表名、字段名这种东西展示给最终用户既不友好也不安全。我在 t3code 里维护了一个简单的错误码约定业务层只抛预设的TRPCError比如NOT_FOUND、FORBIDDEN前端根据error.data.code来决定展示什么样的用户提示而不是原样透传 message。这样做之后线上报错日志也干净了很多定位问题不用在一堆冗余信息里翻。类型错误方面t3code 依赖的是 TypeScript 编译器本身。一旦有人把post.id写成了post._id编辑器立刻标红CI 也跑不过。项目里不需要额外的 lint 规则来约束类型一致性因为类型系统本身就是最强大的 lint。这种让错误出现在最不可能伪装的地方的思路就是我说的类型安全强迫症的最大红利。6. 从开发到上线的避坑清单我在 t3code 里踩过的真实地雷6.1 服务端组件调用 tRPC 的陷阱为什么 useQuery 突然就报错了我最早把PostList直接写在一个 Server Component 页面里心想 Server Component 在服务端运行应该也能用api.post.list.useQuery()吧结果干净利落地报错。原因很简单useQuery是 React hookshooks 只能在客户端组件里调用。Server Component 里根本没有 React 运行时状态管理自然不支持 hooks。解决方式有三种。第一种把需要数据渲染的部分拆成独立的 client component像我前面展示的那样服务端组件负责布局层面客户端组件负责数据交互。第二种在 Server Component 里用server/trpc.ts暴露的createCaller直接以调用者的形式调取数据再把数据作为 props 传给纯展示组件import { createCaller } from ~/server/api/root; import { createTRPCContext } from ~/server/api/trpc; export default async function HomePage() { const ctx await createTRPCContext({ headers: await headers() }); const caller createCaller(ctx); const posts await caller.post.list({ published: true }); return PostsView initialPosts{posts} /; }这种方式性能更好因为不会有额外的 HTTP 往返直接在服务端函数调用。缺点是这些数据不会自动做 React Query 缓存。第三种方式是用 Next.js 推荐的 Server Action 配合 tRPC caller适合处理表单提交逻辑。我建议的项目实践是数据展示优先用createCaller交互性强、需要客户端缓存的状态优先走 client hooks。没有绝对正确的方案看你更在意首屏性能还是交互体验。6.2 NextAuth 会话过期与模棱两可的 401tRPC 受保护 procedure 会给未登录用户抛UNAUTHORIZED。这个错误本身很明确但在 Next.js NextAuth 的集成里有一个环节特别容易出问题getServerSession拿不到 session。明明用户登录了刷新一下页面API 开始报 401。查到最后往往是 NEXTAUTH_SECRET 不一致或者 NEXTAUTH_URL 没配对导致服务端无法校验会话签名。我的排查方法是三层走。第一层看.env里NEXTAUTH_SECRET是不是所有环境都一致本地和生产不要分别生成两份第二层看authOptions配置是否被多个地方重复实例化——如果pages.api.auth.[...nextauth].ts里的配置和server/auth.ts里的配置不是同一个对象session 校验策略可能出现分歧第三层在getServerSession调用处打断点确认session对象到底是null还是被截断。这套排查链路看起来简单但多数组件配合异常问题最后都能归到这三层之一。另外提醒一句不要在前端只依赖session有值来隐藏删除按钮。真正的鉴权必须发生在服务端 tRPC handler 里。前端隐藏按钮只是体验优化服务端FORBIDDEN才是安全兜底。t3code 的删除逻辑里同时做了这两件事前端看不到按钮服务端也会拦截越权请求。6.3 Prisma 连接池本地好好的上线后间歇性 502这个坑我和很多人一样是在部署到 serverless 平台之后才撞上的。本地开发时Node 进程常驻PrismaClient只初始化一次连接池复用得很稳定。一旦部署到 serverless 环境每个冷启动实例都会创建一个新的PrismaClient数据库连接数迅速被耗尽然后就出现间歇性 502错误日志里能看到类似 connection limit reached 的提示。排查过程是这样的先看错误发生的频率和时段如果集中在流量波动之后基本就是连接耗尽再看监控面板里数据库连接数和运行实例数的比值确认每条连接是不是一直挂着不释放。最终方案是给PrismaClient做全局单例并把连接池参数调整到合理范围。代码层面其实很简单import { PrismaClient } from prisma/client; const globalForPrisma globalThis as unknown as { prisma?: PrismaClient }; export const db globalForPrisma.prisma ?? new PrismaClient(); if (process.env.NODE_ENV ! production) globalForPrisma.prisma db;这样在开发环境热更新时不会无限创建实例在生产 serverless 环境也能让单个实例尽量复用。另外我在 Prisma 的连接字符串里显式限制了连接池大小比如?connection_limit5pool_timeout5避免某个实例挂起后把库拖垮。这个是个值得记录的真实教训——不是所有问题都能靠代码修复有时候是架构模式不匹配。6.4 类型推断的边界infer 出来的类型为什么偶尔不对tRPC 的类型推导在 95% 的情况下是自动的但有一种情况会让前端拿到的类型变得诡异在服务端 procedure 里用了Promise.all、条件类型、或者返回了联合类型里包含Date未经 superjson 转换前日期会变成字符串还有undefined字段经过传输后可能丢字段导致联调时发现某个值应该存在却访问不了。我用得比较多的是这两个工具类型inferRouterInputs和inferRouterOutputs。它们可以让你在服务端代码里方便地引用某个 procedure 的输入输出类型比如在测试文件里写类型断言或者在服务端内部做组合操作时复用类型import type { inferRouterInputs, inferRouterOutputs } from trpc/server; import type { AppRouter } from ~/server/api/root; type PostListInput inferRouterInputsAppRouter[post][list]; type PostListOutput inferRouterOutputsAppRouter[post][list];如果你发现推导出的类型和自己预期的不一致优先检查三件事procedure 是否挂错了中间件返回对象里是否混入了Date等非 JSON 安全类型Zod schema 的默认值是否在编译期就被收窄了类型。这三类问题我都遇到过排查一次之后基本就能形成肌肉记忆。7. 从 dev 到上线的部署与后续扩展思路7.1 在 Vercel 部署时的配置要点t3code 是 Next.js 项目直接部署到 Vercel 是最顺的路。部署前有几个配置必须检查环境变量要填全尤其是DATABASE_URL要指向生产数据库、NEXTAUTH_SECRET要和本地一致、AUTH_GITHUB_ID/SECRET要和你的 OAuth 应用一致。构建命令保持prisma generate prisma db push next build的顺序确保 Prisma Client 在编译前已经生成。这里有一个很多人忽略的细节Vercel 的预览部署和正式部署其实是两套环境OAuth 回调地址如果填的是https://your-domain.com/api/auth/callback/github预览分支的回调地址会不一样。解决办法是在 GitHub OAuth 应用里同时配置预览域名和正式域名或者在开发阶段用 Vercel 分配的预览域名临时配置一遍。这是我第无数次被预览环境认证失败搞到心态爆炸之后总结出来的经验。7.2 数据库迁移与种子数据的自动化手动在生产库跑迁移是迟早出事我建议至少把迁移放到 CI 里。最简单的方式是部署前在构建命令里带上prisma migrate deploy它只会执行尚未应用过的迁移文件不会像db push那样直接比对 schema。数据库连接字符串也建议区分部署环境DATABASE_URL单独配置不要在代码里写死。种子数据的自动化我在前面提过。t3code 里的 seed 脚本不仅插入测试数据还会用 NextAuth 的 hash 逻辑创建初始用户方便新环境登录调试。这部分的脚本代码最好保持幂等重复执行不会产生重复数据。我用upsert替代create保证多次prisma db seed不会把表弄脏。7.3 t3code 下一步从模板向生成器演进目前 t3code 还是一个静态模板但我的规划是把它改造成可配置的生成器。现在的使用流程是克隆模板 → 改 schema → 写业务代码。等到生成器阶段流程可以变成执行一条命令 → 选择需要的模块认证方式、数据模型、页面类型→ 自动生成项目。这件事的价值在于不同项目对认证、数据库、样式方案的需求各不相同模板只能满足一部分人生成器却能覆盖更多场景。这期间我还有一些技术升级的打算。比如 tRPC 的 v11 已经支持createCallerFactory的新 API传统中间件和next的写法有一点变化React Query 本身的版本升级也会影响 hooks 的使用方式。这些都需要在模板里同步更新。不过这些都是锦上添花t3code 现阶段最核心的价值依然是一个能让我快速动手、不用在基础设施上反复纠结的全栈起点。最后说一个我自己的体会模板类项目最怕的不是技术选型不够新而是维护者跟不上下游变化。我每隔一段时间都会重新 clone 一次 t3code按文档从零部署一遍看哪里过时了、哪里配置多余了。这个习惯帮我避免了很多模板里跑通了新项目却一直报错的尴尬。如果你也打算维护类似的脚手架这个定期自检的做法非常值得试试。
RELATED

相关推荐

DAMA-CDGA 1406题:数据治理能力的实战压力测试仪

DAMA-CDGA 1406题:数据治理能力的实战压力测试仪

简介:本资源为DAMA-CDGA数据治理认证备考核心题库,面向数据管理从业者、企业数据治理推进人员及准备参加CDGA考试的中高级技术人员。全卷共1406道高质量单选题,严格对标《DMBOK2》知识体系,覆盖机器学习基础分类、数据角色与组织架…

📅 2026/10/15 9:09:02
Autodock Vina批量对接自动化流水线构建指南

Autodock Vina批量对接自动化流水线构建指南

简介:本资源是一份面向计算化学、药物设计领域初学者与科研人员的Autodock Vina批量分子对接实战指南,聚焦Ubuntu 18.04环境下从零部署到集群化运行的完整流程,解决单机算力不足、手动重复对接效率低等实际问题。资源以1个PDF文件&#xff08…

📅 2026/10/15 9:04:01
华为云码道从0到1实战:我用它把课设后端一周的活压到了两天

华为云码道从0到1实战:我用它把课设后端一周的活压到了两天

事情是这样的。这学期软件工程课设,我们组抽到做一个宿舍记账的 Web 应用,周期两周,我负责后端。说实话一开始我挺怵的,FastAPI 我只写过 demo,真要从零搭一个带用户、账单、统计的完整后端,心里没底。 正…

📅 2026/10/15 9:04:01
MORE NEWS

更多资讯

📰

ARM平台室内人群检测与人流进出驱动开发实战

1. 从一个真实需求说起:为什么要在ARM上自己写人流检测驱动去年帮一个做智能楼宇的朋友处理过一件事:他们在一栋写字楼的几个主要出入口装了摄像头,想统计每天进出的人数,用来优化空调和新风系统的运行策略。一开始他们买的是市面…

📰

CTC解码核心:Prefix Score递推与Beam Search实战

CTC训练好之后,真正麻烦的反而是解码。模型在你手上给的只是一张按帧算好的概率表,看起来像一团迷雾:每一帧上哪个字符的概率最高,贪心取一下、再把连续重复和空白去掉,不就是结果吗?很多新手都是这么干的&…

📰

instagram-private-api 创建群聊线程响应详解:DirectRepositoryCreateGroupThreadResponseUsersItem 接口字段全解析

后端 【免费下载链接】instagram-private-api NodeJS Instagram private API SDK. Written in TypeScript. 项目地址: https://gitcode.com/gh_mirrors/in/instagram-private-api 点击查看 免费下载 本篇技术指南以 instagram-private-api 仓库中 docs/interfaces/…

📰

ASP.NET Core实现苹果登录后端验证:identityToken验签与token换取实践

这阵子把某App的苹果登录后端部分接完了,用的技术栈正好是 ASP.NET Core C#。整个过程比想象中麻烦一点:客户端把授权结果丢给你,你看着一个 identityToken 和一个 authorizationCode,第一反应是查文档,第二反应是掉进…

📰

PyTorch加载训练模型预测新数据的四大关键断层与工程化实践

1. 这不是“调个模型跑一下”那么简单:为什么你用训练好的PyTorch模型预测新数据时总出错?“pytorch 使用训练好的模型预测新数据”——这行字在某技术社区里每天被搜索上千次,但真正能一次跑通、结果可信的不到三成。我带过十几期模型部署实…

📰

Java空指针异常(NPE)防御与解决方案

1. 空指针异常的本质与危害Java开发中最常见的运行时异常莫过于NullPointerException(简称NPE),它就像程序中的隐形地雷,随时可能在最意想不到的地方爆炸。我见过太多线上事故的堆栈信息里赫然显示着"NPE"三个字母——轻…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬