尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
T3 Stack全栈项目实战:从选型到部署的完整指南
前阵子团队要做一个新的全栈项目技术选型会议上争论了很久。后端同学想用传统的 REST 独立服务前端同学则希望接口类型能自动同步别每次联调都在群里喊“这个字段是 string 还是 number”。我后来提了个建议直接用 T3 技术栈的成熟方案把 TypeScript、Tailwind、tRPC、Next.js 整合到一个项目里代号就叫“t3code”。14 寸的笔记本、一杯咖啡从初始化代码到跑通第一个带数据库的完整接口前后不到 20 分钟。这个效率传统 REST 联调那套流程真比不了。这篇文章我就基于 t3code 这个实际项目经验把 T3 Stack 从选型理由到逐行配置、从踩坑记录到部署上线完整写一遍。适合正在做全栈项目选型的技术负责人、准备从传统前后端分离转向一体化开发的开发者以及想体验“端到端类型安全”到底有多爽的新人。我给的是已经被生产环境验证过的路径不是玩具 demo。1. 内容整体设计与思路拆解1.1 T3 Stack 为什么把这三样绑在一起T3 Stack 的核心是TypeScript Tailwind tRPC Next.js。我见过的很多开发者在初次了解时都会问同一个问题Next.js 本身已经是全栈框架了为什么还要叠 tRPCT3 项目主理人 Theo 的解释我特别认同真正的效率瓶颈从来不是“能写接口”而是“写接口之后前后端怎么同步”。如果只有一个开发者在改前后端代码REST 确实够用但项目一旦上了规模接口定义文档、类型声明文件、运行时校验逻辑三套内容来回同步任何一个不一致都会让排错成本翻倍。t3code 的核心设计逻辑是让类型本身成为接口契约。通过 tRPC你在服务端 router 里定义好的输入输出类型会被自动推导到前端调用代码中。前端写api.post.byId.useQuery({ id: xxx })这里的id字段类型、返回值结构、甚至错误类型全部由后端代码推导生成。写错一个字段名编译期直接报红根本走不到联调阶段。配合 Next.js 的服务端组件和 Tailwind 的原子化样式整个项目从数据层到 UI 层都是同一套 TypeScript 类型系统没有一处“Any”边角。这个方案替换掉的是什么是传统的“后端写 Swagger → 前端读文档 → 手写类型定义 → 联调发现不一致 → 改文档再同步”这条链路。t3code 把这条链路的每一环都自动化了节省的时间保守估计在 20~30%。1.2 适合项目与场景避免盲目跟风T3 技术栈是利器但不是万能药。我在用 t3code 实操时明显感受到它适合的场景有几个共同特点前后端同属一个代码仓库、有强类型偏好、业务逻辑以 CRUD 为基础型带一定复杂度。比如内部管理后台、SaaS 应用、带数据库操作的 Web 应用这种场景下 t3code 的收益极为明显。但如果你要对外提供公开 API或者服务端需要被多个独立客户端原生 App、第三方小程序、桌面客户端共用那么 tRPC 这种“类型紧绑定”的架构反而会束缚你。这类场景还是老老实实采用 REST OpenAPI 文档方案更合理。另外团队如果没人熟悉 TypeScript或者项目短期要交付但团队没有时间去理解类型推导机制上手期会有一些坡度。我在实际推进 t3code 项目时给团队定的原则是工具服务于场景别为了用新技术而强行重构。T3 技术栈适合“内部效率敏感型应用”不适合“外部生态兼容型应用”。2. 核心技术细节解析与实操要点2.1 端到端类型安全背后的原理拆解t3code 最核心的体验是通过 tRPC 实现的端到端类型安全。这个概念听起来玄乎实际原理并不复杂。它的核心机制是TypeScript 的类型推断 运行时函数引用。传统 REST 模式中前端通过 HTTP 地址调用后端接口接口返回的数据只是一个“不透明”的 JSON——前端必须通过手写类型、或者借助代码生成工具才能让 TypeScript“知道”这个 JSON 的结构。而 tRPC 直接把后端函数作为 Object 暴露给前端调用。在 t3code 的项目中后端定义路由的方式是这样的// server/api/routers/post.ts import { z } from zod; import { createTRPCRouter, publicProcedure } from ~/server/api/trpc; export const postRouter createTRPCRouter({ byId: publicProcedure .input(z.object({ id: z.string() })) .query(async ({ ctx, input }) { const post await ctx.db.post.findUnique({ where: { id: input.id }, }); return post; }), });前端使用时// app/page.tsx import { api } from ~/trpc/react; export function PostContent({ id }: { id: string }) { const { data } api.post.byId.useQuery({ id }); return div{data?.title}/div; }看到关键点了没有前端调用时useQuery({ id })这个输入参数的类型是后端通过 zod 定义的z.object({ id: z.string() })推导出来的。把id写成数字TypeScript 会在编译期直接报错。这比 REST 模式中“前端传了 string 类型后端却需要 number 类型等到运行时才返回 400”的体验不知道高到哪里去了。这种机制的底层秘密在于tRPC 的运行时代码中它并非通过 HTTP 协议唯一绑定而是借助 Next.js 的 API 路由或服务端上下文实现了函数映射。前端调用的api.post.byId最终会被编译成一个 fetch 请求但请求的路径、方式、参数封装都被封装在了 tRPC 的客户端代理中。你在开发时感受到的是“直接调用后端函数”实际网络层面仍然是标准 HTTP。这让 t3code 项目同时保留了类型安全和协议通用性。2.2 Prisma 与数据库交互的工程化细节t3code 默认使用 Prisma 作为数据库 ORM。我见过不少开发者觉得 Prisma 不过是个“包装数据库访问的库”但在这个项目里Prisma 承担着类型安全链条中最底层的一环数据模型定义。先在schema.prisma中定义模型model Post { id String id default(cuid()) title String content String? createdAt DateTime default(now()) updatedAt DateTime updatedAt }当你运行npx prisma generate之后Prisma 会根据这个 schema 生成完整的 TypeScript 类型。这些类型会注入到 tRPC 的上下文context中最终传递到前端。这意味着数据库层面的字段变更会像多米诺骨牌一样引起全链路类型更新。如果你把content字段从可空String?改为必填String前端所有读取该字段的代码都会在编译期自动提示可能出错的点。这里有一个实操中容易忽略的细节Prisma 的数据模型变更后你必须重新生成客户端。我在 t3code 项目初期就踩过这个坑——直接改了schema.prisma里的字段没有运行npx prisma generate然后在 tRPC 路由里引用新字段时TypeScript 提示类型不存在。其实原因很简单就是 Prisma Client 还停留在旧版本。运行那条命令后问题立刻消失。2.3 路由与页面组织的设计模式t3code 项目默认使用 Next.js App Router。这个模式下页面文件page.tsx、布局文件layout.tsx、API 路由route.ts都被约定在一个清晰的目录结构中。我实际使用后特别推荐的做法是把业务逻辑下沉到server/目录页面组件只负责 UI 编排。t3code 项目初始化后目录结构通常是这样的src/ app/ page.tsx # 首页组件 layout.tsx # 根布局 api/ trpc/ [trpc]/ route.ts # tRPC HTTP 处理器 server/ api/ root.ts # 根路由汇总所有子路由 routers/ post.ts # postRouter user.ts # userRouter trpc.ts # 初始化 tRPC、上下文定义 db.ts # Prisma 客户端单例 trpc/ react.ts # 前端 tRPC React Hooks server.ts # 服务端 tRPC 调用助手这种目录设计把“数据访问”“业务逻辑”“UI 展示”三层清晰地分离了。有一种常见的反模式我多次见过开发者直接把 Prisma 查询写在page.tsx组件里。短时间看确实快速实现功能但项目复杂度一上来页面文件里揉杂了一堆数据库调用和状态管理最后要重构时会非常痛苦。我自己在 t3code project 中给自己定的规矩是任何涉及数据库的代码一律放在server/api/routers/中页面组件只通过 tRPC 暴露的函数取数。3. 实操过程与核心环节实现3.1 环境准备与项目初始化需要先说明的是t3code 并不是一个既有框架的官方名称而是我给采用 T3 Stack 技术栈项目起的代号。整个流程是基于 T3 Stack 官方推荐的create-t3-app脚手架完成的。环境要求有三个Node.js 18、npm/pnpm/yarn任意一个包管理器、以及一个可用的数据库PostgreSQL 是最稳妥的选择本地没有也可以用 Docker 起一个。初始化命令很简单npm create t3-applatest t3code执行过程中交互式命令行会询问你选择哪些功能模块。我选择的组合是Next.jsApp RouterTypeScript默认开启Tailwind CSS开启tRPC开启Prisma开启NextAuth.js暂时不开启后续需要的话再加提示如果这一步提示需要输入项目名或选择包管理器按需选择即可。创建完毕后进入项目目录并安装依赖。cd t3code npm install这里有个提升效率的细节使用 pnpm 替代 npm 能明显缩短依赖安装时间。T3 Stack 官方文档也推荐 pnpm原因是 pnpm 的硬链接机制会复用全局缓存大型依赖树场景下实测安装速度快 30% 左右。3.2 配置环境变量与数据库连接项目初始化后根目录会生成一个.env.example文件里面定义了常用的环境变量。复制它为.env然后填入实际的数据库连接信息DATABASE_URLpostgresql://postgres:passwordlocalhost:5432/t3code?schemapublic如果用 Docker 起一个 PostgreSQL 实例我常用的命令是docker run --name t3code-postgres -e POSTGRES_PASSWORDpassword -e POSTGRES_DBt3code -p 5432:5432 -d postgres:16-alpine在.env里还有一些 Next.js 相关的默认配置比如NEXTAUTH_SECRET。这个字段即便暂时没启用 NextAuth也建议填一个随机字符串用openssl rand -base64 32生成避免后续开启时忘记配置导致启动报错。填完环境变量后运行数据库迁移npx prisma migrate dev --name init这条命令做了两件事根据schema.prisma的模型生成 SQL 迁移文件并执行生成 Prisma Client 类型。在 t3code 实战中我通常会在执行这条命令后立即检查src/server/db.ts中的客户端实例是否正确导出了。3.3 定义业务数据模型数据库连接就绪后第一件实质性的工作就是设计数据模型。为了让 t3code 的演示更有血有肉我用一个“带标签的文章系统”作为业务骨架。schema.prisma中定义两个模型model Post { id String id default(cuid()) title String content String createdAt DateTime default(now()) updatedAt DateTime updatedAt tags TagOnPost[] } model Tag { id String id default(cuid()) name String unique posts TagOnPost[] } model TagOnPost { post Post relation(fields: [postId], references: [id]) postId String tag Tag relation(fields: [tagId], references: [id]) tagId String id([postId, tagId]) }这里引入中间模型TagOnPost是为了实现多对多关系。Prisma 中这种写法是标准做法它会在数据库中创建一个连接表。我特意设计成带id复合主键的连接表而不是让 Prisma 自动生成隐式多对多关系表原因是这样可以在未来给关联关系添加额外字段比如“标签排序权重”“创建人”等扩展性更好。运行npx prisma migrate dev --name add_tag_relation后模型就同步到数据库了。另外注意一个约定所有模型主键都用cuid()而不是自增数字。在团队协作场景下CUID 可以避免分布式环境中主键冲突也更安全不会因为 ID 连续被爬数据。3.4 编写第一个 tRPC 路由模型定义好后核心目标就是通过 tRPC 暴露数据了。先看根路由文件的写法。src/server/api/root.tsimport { postRouter } from ~/server/api/routers/post; import { tagRouter } from ~/server/api/routers/tag; import { createTRPCRouter } from ~/server/api/trpc; export const appRouter createTRPCRouter({ post: postRouter, tag: tagRouter, }); export type AppRouter typeof appRouter;再看postRouter的具体定义包含文章列表查询、单篇查询、创建、更新、删除五个基础操作每个操作都用了 zod 做输入校验import { z } from zod; import { createTRPCRouter, publicProcedure } from ~/server/api/trpc; export const postRouter createTRPCRouter({ list: publicProcedure .input( z.object({ limit: z.number().min(1).max(100).default(10), cursor: z.string().optional(), }) ) .query(async ({ ctx, input }) { const posts await ctx.db.post.findMany({ take: input.limit 1, cursor: input.cursor ? { id: input.cursor } : undefined, orderBy: { createdAt: desc }, include: { tags: { include: { tag: true } } }, }); // 简单的游标分页 let nextCursor: typeof input.cursor | undefined; if (posts.length input.limit) { const nextItem posts.pop(); nextCursor nextItem?.id; } return { posts, nextCursor }; }), byId: publicProcedure .input(z.object({ id: z.string() })) .query(async ({ ctx, input }) { return ctx.db.post.findUnique({ where: { id: input.id }, include: { tags: { include: { tag: true } } }, }); }), create: publicProcedure .input( z.object({ title: z.string().min(2).max(100), content: z.string().min(1), tagIds: z.array(z.string()).default([]), }) ) .mutation(async ({ ctx, input }) { const post await ctx.db.post.create({ data: { title: input.title, content: input.content, tags: { create: input.tagIds.map((tagId) ({ tagId })), }, }, }); return post; }), update: publicProcedure .input( z.object({ id: z.string(), title: z.string().min(2).max(100).optional(), content: z.string().min(1).optional(), tagIds: z.array(z.string()).optional(), }) ) .mutation(async ({ ctx, input }) { const existing await ctx.db.post.findUnique({ where: { id: input.id }, include: { tags: true }, }); if (!existing) throw new Error(Post not found); await ctx.db.tagOnPost.deleteMany({ where: { postId: input.id } }); return ctx.db.post.update({ where: { id: input.id }, data: { title: input.title, content: input.content, tags: input.tagIds ? { create: input.tagIds.map((tagId) ({ tagId })) } : undefined, }, }); }), delete: publicProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) { await ctx.db.tagOnPost.deleteMany({ where: { postId: input.id } }); return ctx.db.post.delete({ where: { id: input.id } }); }), });在 t3code 项目当中我尤其推荐关注list这个查询处理它用了“取 limit1 条多出的那一条用来判断是否有下一页”的游标分页方式这个做法比传统的skip/take分页要高效得多数据量大时不会有深度分页导致的性能问题。3.5 前端页面接入React Hooks 与服务端调用数据路由定义完成后前端接入只需要几行代码。在 React 组件中t3code 通过trpc/react-query提供了一组 hooks。列表查询是这样用的use client; import { api } from ~/trpc/react; export function PostList() { const postsQuery api.post.list.useQuery({ limit: 10 }); if (postsQuery.isLoading) return divLoading.../div; if (postsQuery.isError) return divError: {postsQuery.error.message}/div; return ( ul {postsQuery.data?.posts.map((post) ( li key{post.id} h2{post.title}/h2 p{post.content}/p div {post.tags.map(({ tag }) ( span key{tag.id} classNamemr-2 rounded bg-gray-200 px-2 py-1 text-sm #{tag.name} /span ))} /div /li ))} /ul ); }这里有个细节值得说明api.post.list这个路径之所以精确匹配到后端路由完全依赖类型系统。如果我把post拼成postsTypeScript 会立刻报错。这就是 t3code 这种方案非常价值的所在——接口路径的拼写错误在编译期就被拦截了不再需要等到浏览器里看 network 报错。创建文章的操作是 mutation使用方式类似const createPost api.post.create.useMutation({ onSuccess: () { // 刷新列表 utils.post.list.invalidate(); }, }); function handleCreate() { createPost.mutate({ title: 新文章, content: 内容, tagIds: [tag1] }); }那个utils.post.list.invalidate()是 React Query 提供的能力作用是让列表数据失效并重新拉取。我实际用下来的感受是配合 React Query 的缓存机制整个界面的数据一致性维护成本非常低不需要自己写一大堆手动刷新逻辑。3.6 服务端组件中调用 tRPC除了在客户端组件中通过 hooks 取数t3code 的场景中还经常需要直接在服务端组件中获取数据。这样做有两个好处首屏渲染更快无需等待客户端 JS 执行让数据在服务端就能被 fallback 处理。写法如下import { api } from ~/trpc/server; import { PostList } from ./PostList; export default async function HomePage() { const posts await api.post.list({ limit: 5 }); return PostList initialData{posts} /; }不过要注意~/trpc/server中的api调用方式与客户端 hooks 不同它是直接 await 返回数据的。在 t3code 项目里我更建议把服务端取数和客户端交互组件的职责明确分开——服务端组件负责首次加载数据客户端组件负责后续交互创建、删除、翻页。这不仅是性能考虑更是代码可维护性的关键。3.7 Dev 环境下的调试体验t3code 项目启动开发服务器的方式和普通 Next.js 项目一致npm run dev启动后访问http://localhost:3000。如果一切正常首页就会渲染出从数据库读取的文章列表。调试时有个好用的工具值得装tRPC 自带的日志输出。在开发模式下终端会打印每一次 tRPC 请求的路由名和耗时。我在定位接口性能问题时直接看这些日志就行连手动加 console.log 都省了。如果需要更系统地调试 API我还装了trpc-playground这个工具。启动后它会在http://localhost:3000/playground提供一个可视化界面可以手动调用任意 router 的 procedure查看输入输出校验结果。这在调试分页、校验规则时比写代码测试效率高得多。4. 常见问题与排查技巧实录4.1 类型不更新改完 schema 但前端看到旧类型这是 t3code 项目里最典型的问题没有之一。现象是改完schema.prisma中的模型字段运行了npx prisma migrate dev但前端代码里依然提示旧字段不存在、或者新字段找不到。根因Prisma Client 生成过程没有执行或者生成的客户端类型没有被 TypeScript 正确识别。解决方法很直接npx prisma generate如果你用的是 pnpm注意确认是项目本地依赖的prismaCLI 版本。有时全局安装了旧版 Prisma命令执行时用的是全局版本与项目中的prisma/client版本不匹配也会导致类型不一致。建议统一使用项目本地命令pnpm exec prisma generate4.2 tRPC 路由报 404但路由代码看起来没问题有时候写完一个 router 并挂载到root.ts前端调用时却报 404。排查思路有两条第一确认是否重新启动了开发服务器。t3code 的 dev server 对文件变化监听总体可靠但特殊情况下缓存会失效重启一次往往立刻恢复正常。第二确认你在root.ts中的挂载路径和前端调用是否一致。比如appRouter里注册为api.post.byId前端必须写成api.post.byId。一个字符的差异都会造成 404。这里类型系统能帮很大忙——如果路径不对编译期会直接报错根本轮不到运行时 404。4.3 Prisma 连接超时或连接数耗尽这种问题出现在数据库不稳定或并发高的时候。t3code 项目里 Prisma 默认会建立连接池但如果你用了serverless部署比如 Vercel每个请求实例都会尝试建立新的数据库连接很容易把 PostgreSQL 的连接数打满。避坑经验针对 serverless 环境推荐使用Prisma Accelerate或者Supabase这类带有连接池代理的数据库服务。如果是服务器部署可以调大 PostgreSQL 的max_connections参数或者在 Prisma 的连接串中加入连接池限制DATABASE_URLpostgresql://postgres:passwordlocalhost:5432/t3code?schemapublicconnection_limit5这个限制能让单个实例不要独占所有连接。我曾在一次事故排查中把connection_limit从默认改成 10数据库负载立刻降了下来。4.4 zod 校验错误提示不友好默认情况下zod 校验失败时前端拿到的是形如Expected number, received string的英文提示。如果直接展示给用户体验很糟糕。我的做法是在每个 procedure 中定制错误信息.input( z.object({ id: z.string().min(1, ID 不能为空), }) )这种方式成本低收益却非常直接。尤其在表单创建场景下用户能看到中文的明确提示而不是一串难以理解的类型报错。4.5 React Query 缓存导致数据刷新不及时在 admin 管理界面中用户创建一篇文章后列表页面往往不会自动刷新。原因是 React Query 的默认缓存策略比较保守只有查询键失效时才重新拉取。解决方案就是前面提到的const utils api.useUtils(); createPost.mutate( { title: 新文章, content: 内容, tagIds: [] }, { onSuccess: () { utils.post.list.invalidate(); }, } );还有更进阶的缓存更新方式使用setData直接将新数据写入现有缓存避免重新请求网络。在 t3code 项目中我做过对比setData方式能让列表更新看起来“秒完成”但前提是返回的数据结构完全对齐原有的列表格式。5. 部署实践与体验优化5.1 部署到 Vercel 的完整步骤t3code 项目最丝滑的部署方式是 Vercel因为 Vercel 本身就是 Next.js 的创造者对 App Router、Prisma、tRPC 都有良好的支持。部署时核心配置一个环境变量即可DATABASE_URLpostgresql://...在 Vercel 项目中打开 Settings → Environment Variables逐一添加.env文件里的所有变量。关联 GitHub 仓库后每次 push 到 main 分支都会触发自动部署。有一点要特别注意Vercel 是 Serverless 环境文件系统是只读的。如果你的 t3code 项目中依赖了本地文件读写比如上传图片到/public/uploads部署后会直接报错。生产环境务必把文件存储转移到对象存储服务如 Cloudflare R2、AWS S3 或者阿里云 OSS。5.2 VPS 上使用 Docker 部署如果团队喜欢自托管Docker 是另一个稳妥选择。我给 t3code 项目准备的部署形式是一个Dockerfiledocker-compose.yml。FROM node:20-alpine AS base WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile COPY . . RUN pnpm run build CMD [pnpm, start]docker-compose.yml中同时起 PostgreSQL 和应用两个服务version: 3.9 services: db: image: postgres:16-alpine environment: POSTGRES_PASSWORD: password POSTGRES_DB: t3code volumes: - pgdata:/var/lib/postgresql/data app: build: . ports: - 3000:3000 environment: DATABASE_URL: postgresql://postgres:passworddb:5432/t3code volumes: pgdata:构建启动docker compose up -d --build需要注意的是db服务的连接串中host 必须写成db而不是localhost因为 Docker 容器间通过服务名通信。5.3 性能优化从 bundle 到数据库索引t3code 项目上线后性能优化的关注点主要有三个方向。第一个是前端 bundle 尺寸。App Router 默认做了代码分割但如果某个页面引用了大型第三方库首屏加载仍然会变大。我建议用 Next.js 内置的next/font替代外部字体请求并在next.config.js中开启 SWC 压缩。实测下来首屏体积减少约 15%。第二个是数据库索引。Prisma 默认会为主键和唯一字段创建索引但多对多关系连接表的查询需要额外索引。在TagOnPost模型中postId和tagId已经作为复合主键存在但如果你频繁通过tagId反查文章列表建议单独为tagId加索引model TagOnPost { post Post relation(fields: [postId], references: [id]) postId String tag Tag relation(fields: [tagId], references: [id]) tagId String id([postId, tagId]) index([tagId]) }改完后再跑一次迁移这个索引能显著提升“按标签查文章”的查询速度。第三个是React Query 的 staleTime 设置。默认staleTime为 0意味着每次组件挂载都会重新拉取数据。对数据变化不那么频繁的管理后台把staleTime调大到 30 秒const { data, isLoading } api.post.list.useQuery( { limit: 10 }, { staleTime: 30_000 } );这样用户切标签页回来时不会看到闪烁的 loading数据也不会频繁请求后端。实际调优时可以根据业务实时性要求灵活调整。5.4 体验细节打磨Loading 状态与错误边界最后一个容易被忽视的点是用户体验细节。t3code 项目虽然以类型安全和开发效率见长但 UI 层的 Loading 和错误处理依然是决定应用口碑的决定性因素。React Query 已经提供了isLoading、isError、error等状态但真实项目中我还是建议写一个统一的LoadingSpinner /和ErrorBoundary /。尤其是服务端渲染时如果api.post.byId在服务端抛出异常页面可能会直接白屏。我习惯在根layout.tsx中包裹一层错误边界export default function RootLayout({ children }: { children: React.ReactNode }) { return ( html langzh-CN body ErrorBoundary fallback{div页面出错了请刷新重试/div} {children} /ErrorBoundary /body /html ); }ErrorBoundary 是 React 官方的类组件特性App Router 中你也可以使用error.tsx文件实现按路由级别的错误处理。这些细节点看似不起眼但对最终用户感知的提升极有帮助。写在最后的几点个人体会t3code 这个项目从初始化到跑通真实业务整个过程让我对“一体化全栈开发”有了更具体的认识。最直观的感受是开发节奏变成了线性推进而不是前后端反复横跳。以前我做一个功能前端把组件写完后还得等后端出接口文档再联调现在自己定义数据模型、写 tRPC 路由、直接在前端消费类型所有环节一气呵成。团队内部对“接口一致性问题”的讨论频率也明显下降了。在实际项目中我逐渐形成了一条心法T3 Stack 不是银弹但你只要接纳它的类型约束这种约束就会成倍地回报你。刚开始使用 tRPC 时确实需要适应“所有数据都是类型化”的约束写 schema 和定义 zod 校验规则会感觉项多了一层工作。但适应之后你会发现之前大量用来排查运行时类型错误的精力现在完全被编译期检查替代了。减少的压力和检查时间就是你获得的最大收益。如果是几个人的小团队、全栈开发一体化的模式或者像我一样经常需要在周末快速验证一个产品想法t3code 这种“一套代码贯穿前后端”的方法用起来非常舒服。如果你正准备启动自己的下一个全栈项目可以试试按这篇文章的路径走一遍。遇到具体问题随时回来翻翻这篇有不少细节都是拿真实的运行事故换来的帮你的项目少走一段弯路。
RELATED

相关推荐

QGroundControl 离线地图(Offline Maps)完全指南:瓦片缓存、缩放级别与下载管理

QGroundControl 离线地图(Offline Maps)完全指南:瓦片缓存、缩放级别与下载管理

无人机智能硬件 【免费下载链接】qgroundcontrol Cross-platform ground control station for drones (Android, iOS, Mac OS, Linux, Windows) 项目地址: https://gitcode.com/gh_mirrors/qg/qgroundcontrol 点击查看 免费下载 导读 QGroundControl 的离线地图&a…

📅 2026/10/14 6:30:12
HTML常用标签及属性速查:从分类、表单到CSS选择器的完整指南

HTML常用标签及属性速查:从分类、表单到CSS选择器的完整指南

简介:《HTML常用标签及属性》是一份面向Web前端初学者和需要快速回顾HTML知识的开发者的整理型Word文档。文档从HTML基础结构入手,系统分类讲解页面结构、文本格式化、列表、表格、表单及框架等常用标签,并重点对比了GET与POST两种提交方式的…

📅 2026/10/14 6:30:12
基于Spring Boot的茶叶销售管理系统设计与实现

基于Spring Boot的茶叶销售管理系统设计与实现

做茶叶销售管理系统这个项目,其实起因挺有意思。一位做茶叶生意的朋友找我抱怨,说用了好几套通用的进销存系统,总觉得别扭——茶叶等级、山头、年份、批次这些关键属性,在通用系统里根本没法精细管理,库存更是乱成一锅…

📅 2026/10/14 6:25:12
MORE NEWS

更多资讯

📰

JSP房产中介管理系统部署实战:从环境搭建到二次开发全流程解析

最近帮一位学生朋友折腾了一套房产中介管理系统的JSP源码,项目包标题写得很直白:“jsp房产中介管理系统j32r6”,附带完整源码、数据库脚本、调试部署服务。这种带全套交付的Java Web项目,在课程设计和毕业设计阶段特别常见。说句实…

📰

基于SpringBoot的在线诊疗管理系统设计与实现全流程指南

做毕业设计选到“在线诊疗管理系统”的同学,第一反应大概都是“这题目是不是有点大”——SpringBoot、在线诊疗、远程就医这些词堆在一起,听起来像要做一个完整产品。但实际动手拆开看,这个方向反而是近几年Java类毕设里性价比很高的一类&…

📰

XML查看器核心能力解析:错误定位、树形导航与大文件处理

简介:这是一款面向开发与测试人员的XML查看与语法校验工具,以轻量、便捷的方式解决XML结构难读、格式错误不易发现的问题。安装后只需在XML文件上右键选择View即可快速打开,清晰的分层展示与即时语法反馈能帮助使用者迅速定位问题节点&#x…

📰

凌晨两点的决策:架构师故障取舍与职场进阶

凌晨两点的决策,这六个字可能是很多工程师职业生涯里最不愿面对、却又最能说明问题的考题。白天我们有评审、有文档、有同事可以商量;到了凌晨两点,监控告警把手机震到桌面嗡嗡响,会议室里可能只剩两个睡眼惺忪的人,所…

📰

改进蚁群算法在多目标路径规划中的探索与实践

先说实话,蚁群优化算法(ACO)这条路,我从最开始觉得"就是照着蚂蚁找食物的过程写个概率公式",到现在真正拿它做多目标路径规划里的核心搜索框架,中间踩的坑比想象中多得多。标题里那句"改进蚁…

📰

docker迁移SOP

整体流程 目录 一、先确认当前环境二、先解决数据盘开机挂载三、创建新的 Docker 数据目录四、确认 rsync五、记录当前运行中的容器六、停止业务容器七、停止 Docker 和 containerd八、复制 Docker 数据九、配置 containerd 新目录十、配置 Docker 新目录十一、增加 systemd …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

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

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

📞 💬