☰
全栈工程实践:基于Next.js、tRPC与Prisma搭建类型安全基线
2026/10/26 23:29:05 网站建设 项目流程

在本地一个叫t3code的目录里,我攒了将近一年折腾出来的全栈工程实践。它不是某个开源库,也不是某个框架,而是一套我把 TypeScript、Tailwind CSS、tRPC 这些“三T”技术真正揉在一起之后沉淀出的可复用样板。好多朋友问我为什么放着现成的脚手架不用,非要自己攒一套,答案很简单:大多数脚手架能帮你把项目跑起来,但不会帮你把类型安全、权限边界、数据校验、部署一致性这些“看不见的基建”一次性理顺。这篇就把t3code的完整设计思路、目录结构、技术选型理由和踩坑记录全部分享出来,适合正在做一个中大型 Next.js 全栈项目,或者想从零搭建一套能直接开工的工程基线的读者。

1.t3code想解决的问题:不是又一个脚手架,而是类型安全优先的全栈基线

1.1 为什么样板工程比业务代码更难写

我在几年前接手过一个项目,前后端分离,接口文档用 YApi 维护。每次后端改字段,前端要等文档更新,联调时对着 Postman 猜类型。项目到后期,一个订单接口传错参数的概率极高,排查一遍下来大半天就没了。这个经历让我下定决心:如果自己搭项目,一定要让“类型”从数据库一直贯穿到前端组件的 props,中间不允许出现任何无类型的边界。

但真开始做之后发现,全栈样板远比想象中复杂。你需要考虑数据库模型怎么导成 TypeScript 类型,API 层怎么在不写手写类型的情况下自动推导,前端在调用接口时能不能获得完整的补全和编译期检查;还需要考虑登录状态怎么注入到服务端逻辑里,权限判断是放前端路由还是后端中间件。这些点每一个都值得单独写篇文章,而样板工程把这些问题全部堆在同一个代码库里,复杂度是指数级上升的。

t3code本质上就是我自己对这些问题的答案汇总。它不是给小白准备的一键启动工具,而是给那些已经写过业务、被类型问题或者联调效率折磨过的人,一个可以直接拿走的基线。

1.2t3code的定位与设计目标

我给自己定的设计目标可以用三句话概括:第一,前后端共享类型,减少手动定义接口类型的成本;第二,所有服务端入口都有统一的校验和错误处理,不允许出现裸奔的try/catch或者忘写校验的接口;第三,目录结构按业务功能组织,而不是按controller/service/model这种技术角色分层。

这里多说一句为什么第三点很重要。传统分层模式下,一个“创建订单”的功能会横跨 controller、service、model 三个目录,改一个需求要跳三个文件夹。按功能组织的话,订单相关的路由、服务、校验、测试都放在modules/order下面,一个需求基本只动一个目录。对于中小型团队来说,这种组织方式维护起来特别直观。

在设计目标之外,我还在README里明确写了非目标:不刻意兼容老旧的 JavaScript 项目,不做多端复用,不搞微前端。这不是说这些方向不好,而是样板工程的边界必须清晰。如果什么都往里面塞,最后的结果一定是配置比业务代码还多,反而失去了参考价值。

1.3 我用一张表格验收“完成度”

在开发t3code的过程中,我用一张验收表来检查这个基线到底合不合格。这张表也推荐给你在搭自己的工程时使用,它可以帮你判断一个样板是否真正可用:

验收项具体要求拿到什么效果
端到端类型安全数据库字段变换到前端组件,全过程无any前端调用接口代码就能拿到完整类型补全
接口统一校验每个 tRPC procedure 或 API Route 必须有输入校验非法参数不可能漏进业务逻辑
权限可追踪需要登录的接口在代码层面强制校验少一个requireAuth直接编译报错或运行时被拦截
配置可复制新环境从 clone 到跑通开发环境不超过十分钟新人上手成本可控
部署无惊喜本地跑通的逻辑,生产环境不因环境差异爆雷减少“在我机器上是好的”这类问题

2. 技术选型复盘:为什么锁定 Next.js、tRPC 和 Prisma 的三角组合

2.1 路由与渲染层:Next.js App Router 带来的边界价值

在选择路由层时,我对比过纯 Express 后端加 React 前端的方案,也对比过 Nuxt。最终锁定 Next.js App Router,核心原因是它在服务端组件和客户端组件之间画了一条清晰的分界线。像详情页这类需要数据但不需要太多交互的页面,直接在 Server Component 里取数;像表单页这类有大量状态的页面,则标记为 Client Component。框架层面的边界一旦定死,团队成员不太会写出“把所有数据请求都放 useEffect 里”这种代码。

App Router 的另一个价值是支持 Route Handler 与 tRPC 并存。t3code里大部分数据操作都走 tRPC,但偶尔需要提供给第三方或外部系统的接口时,可以直接写一个标准 Route Handler 返回 JSON,两边不冲突。这个灵活性让样板工程不会成为绑死你的枷锁。

2.2 数据查询层:tRPC 为什么比手写 REST 更适合内部服务

很多人对 tRPC 的第一印象是“它不是 REST”,觉得不标准。但我们需要想清楚:这套 API 是给谁用的?如果是开放给未知的第三方开发者,那 REST 和 OpenAPI 规范很有用;如果只是你自己的 Next.js 前端调用自己的 Next.js 后端,那毫无疑义应该选择 tRPC——因为你不需要在每个接口手写类型定义,类型直接从函数签名推导出来,前端收到的响应类型就是后端函数的返回类型。

t3code里一个典型的 tRPC mutation 长这样:

// server/modules/task/router.ts import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "@/server/trpc/trpc"; export const taskRouter = createTRPCRouter({ create: protectedProcedure .input( z.object({ title: z.string().min(1).max(100), priority: z.enum(["LOW", "MEDIUM", "HIGH"]), dueDate: z.string().optional(), }) ) .mutation(async ({ ctx, input }) => { return ctx.db.task.create({ data: { title: input.title, priority: input.priority, dueDate: input.dueDate, ownerId: ctx.session.user.id, }, }); }), });

调用端不需要单独维护 URL、请求方法、参数类型,直接导入该 router 导出的类型:

// app/tasks/new/page.tsx "use client"; import { api } from "@/trpc/react"; export default function NewTaskPage() { const utils = api.useUtils(); const createTask = api.task.create.useMutation({ onSuccess: () => utils.task.list.invalidate(), }); // createTask.mutate({ title: "...", priority: "HIGH" }) }

对比一下,假设后端有 80 个接口,REST 方案里你至少需要维护 80 个请求函数和 80 个响应类型,哪怕用 OpenAPI 生成器也逃不掉代码生成步骤;tRPC 方案里这 80 个接口的客户端调用代码完全由框架推导生成,少写的不只是代码,更是大量的沟通成本和出错空间。

2.3 ORM 与样式方案:Prisma 和 Tailwind CSS 的取舍

ORM 我选了 Prisma。原因很实在:Schema 即类型来源。写一个schema.prisma,跑一下prisma generate,所有数据库模型对应的 TypeScript 类型就齐了,而且是数据库层和类型层同步生成,不会出现“类型说这个字段存在、数据库里却没有”的情况。Prisma 的迁移体系虽然在某些边缘情况下需要手改 SQL,但对于绝大多数应用来说足够好用。

样式层选择了 Tailwind CSS,这个不多解释。需要强调的是,t3code里不是简单装一个 Tailwind 就完了,而是把颜色、间距、字号等设计令牌统一放进了tailwind.config.ts的 extend 里。这样做的好处是:组件里不出现魔数颜色,比如#1d4ed8这种直接写死的值,而是统一用primary-DEFAULT这类语义变量。后期换主题或者做暗色模式时,只需要改配置文件,不用全项目替换色值。

2.4t3code的最终依赖清单

把核心依赖列出来,方便你对比自己的项目:

类型依赖用途
框架next、react、react-dom应用框架与渲染层
语言typescript全链路类型检查
数据层@prisma/client、prisma数据库模型与迁移
API 层@trpc/server、@trpc/client、@trpc/react-query、@trpc/next类型安全数据接口
校验zod输入输出校验
认证next-auth、next-auth-session 适配器登录与会话管理
样式tailwindcss、@tailwindcss/forms原子化样式方案
工具eslint、prettier、dotenv-cli代码质量与环境管理

这份清单里我特别删掉了 axios。所有 HTTP 请求都走 tRPC 客户端或原生fetch,没必要再引一个重量级网络库。Redux 也没有出现,因为 tRPC 配合 React Query 的缓存机制已经覆盖了大部分服务端状态管理需求,本地状态用 React Hook 就足够。

3. 核心架构拆解:一个请求在t3code中怎么流转

3.1 从点击按钮到数据库写入的完整链路

理解了依赖为什么这样选之后,我们把视角拉到运行时,看一个典型的“创建任务”请求在t3code中是怎么流转的:

  1. 前端组件调用api.task.create.useMutation()得到的mutate方法,传入参数对象。
  2. tRPC React Query 客户端收到参数,先进行类型检查(编译期已经在做的事),然后把请求发给 Next.js 的 API 路由/api/trpc/*。
  3. tRPC 服务端中间件根据路由名task.create找到对应 procedure。
  4. protectedProcedure先执行认证中间件:从 session 中取用户,没有则抛出UNAUTHORIZED错误,请求到此结束。
  5. zod校验器执行,对输入参数逐字段检查,不合法则抛出BAD_REQUEST并携带具体字段错误。
  6. 校验通过后执行 mutation 函数体,在函数内部拿到ctx.session.user.id和已经类型化的input。
  7. Prisma 客户端执行数据库写入,返回新任务对象。
  8. 整个链路的结果沿着调用链回传,前端拿到数据后,onSuccess回调触发,React Query 自动清理相关缓存。

这条链路里最关键的设计是:认证、校验、数据库这三个环节在框架层面就已经被强制串起来了,业务开发人员写 router 的时候根本不需要自己想“要不要加个 if 判断用户是否登录”。protectedProcedure这个封装已经把权限卡死在入口处,后面写的人想跳过都难。

3.2 Router 与 Context 的设计细节

t3code的 trpc 上下文设计是这样的:中间件负责把session和db塞进ctx,后续所有 procedure 都能直接读取。

// server/trpc/trpc.ts import { initTRPC, TRPCError } from "@trpc/server"; import { type CreateNextContextOptions } from "@trpc/server/adapters/next"; import { getServerSession } from "next-auth"; import { authOptions } from "@/server/auth"; import { prisma } from "@/server/db"; export const createTRPCContext = async (opts: CreateNextContextOptions) => { const session = await getServerSession(authOptions); return { session, db: prisma, ...opts, }; }; const t = initTRPC.context<typeof createTRPCContext>().create(); const isAuthed = t.middleware(({ ctx, next }) => { if (!ctx.session?.user) { throw new TRPCError({ code: "UNAUTHORIZED" }); } return next({ ctx: { ...ctx, session: { ...ctx.session, user: ctx.session.user }, }, }); }); export const createTRPCRouter = t.router; export const publicProcedure = t.procedure; export const protectedProcedure = t.procedure.use(isAuthed);

关键点在于中间件里重新展开了ctx。这样做让后续 procedure 的ctx.session.user类型不再是User | null,而是非空User,编译器和 IDE 都能直接推断出来,不需要在业务代码里做大量的空值收窄。很多人搭 tRPC 项目时没有做这一步,导致每个 procedure 里都有一堆if (!ctx.session?.user) throw,代码看起来又啰嗦又容易漏——中间件设计的价值就在这里,它把“需要登录”这个语义变成一个强类型约束。

3.3 Zod 校验与错误处理策略

校验策略上,我遵循“input 永远要有一个 zod schema”的原则。就算某个接口目前不需要任何参数,我也写一个z.object({}).strict()。这样当未来有人给接口加参数却忘记更新校验时,多传的字段会直接报错,不会出现“传了但没被处理”的静默失败。

错误处理方面,t3code里统一约定:外部错误(参数不合法、未登录、无权限)由 Zod 和 auth 中间件抛TRPCError;内部错误(数据库异常等)直接向外抛出并记录到日志系统。开发环境里为了调试方便,我让所有错误都在控制台打印完整堆栈;生产环境则通过一个 error formatter 过滤掉内部堆栈信息,只对外暴露错误码:

// server/trpc/error-format.ts import { TRPCError } from "@trpc/server"; export function formatError(error: TRPCError) { if (error.code === "INTERNAL_SERVER_ERROR") { console.error(error); return { message: "Internal server error", code: error.code }; } return { message: error.message, code: error.code }; }

这个过程解决了一个很常见的困扰:后端报错信息太详细会把数据库表结构或者文件路径暴露给用户,太简略又会让前端排查困难。格式化后,业务能明确处理的错误有结构化信息,不能处理的则统一收敛到“内部服务器错误”,既不泄露细节,也不增加前端判断复杂度。

4. 目录结构与可复用模块:按业务功能而不是按技术角色组织

4.1t3code的目录骨架

完整的代码结构如下,我把核心部分做了注释说明:

t3code/ ├── app/ │ ├── (auth)/ # 登录、注册相关路由组 │ ├── dashboard/ # 需要登录的页面 │ ├── api/trpc/[trpc]/route.ts # tRPC 入口 │ └── layout.tsx ├── modules/ │ ├── task/ │ │ ├── router.ts # tRPC 子路由 │ │ ├── schemas.ts # Zod 校验器 │ │ ├── services.ts # 纯业务逻辑(可单独测试) │ │ └── components/ # 该模块专属组件 │ ├── user/ │ │ ├── router.ts │ │ ├── schemas.ts │ │ └── services.ts │ └── project/ ├── server/ │ ├── auth.ts # NextAuth 配置 │ ├── db.ts # Prisma 单例 │ └── trpc/ │ ├── trpc.ts # tRPC 初始化与上下文 │ ├── caller.ts # 服务端直接调用 tRPC 的封装 │ └── error-format.ts # 统一错误格式化 ├── prisma/ │ └── schema.prisma ├── components/ │ ├── ui/ # 通用 UI 组件 │ └── layout/ # 布局相关 ├── lib/ │ ├── utils.ts # 通用工具函数 │ └── validators.ts # 全局校验函数 └── tailwind.config.ts

你可能注意到我把modules这个目录放得比server还靠前。这是有意为之。server下面只放框架级的初始化代码,业务逻辑全部下沉到modules。这样做的实际收益是:当你有一天想把某个模块拆成独立微服务时,整个目录几乎可以一比一搬出去,因为模块内部的 router、service、schema 都是自洽的,不依赖全局的命名空间。

4.2 server 端三件套:db.ts、auth.ts与trpc.ts

server/db.ts是 Prisma 客户端的单例封装。这个看着简单,但有个细节值得提:在开发环境下,热更新机制会导致PrismaClient被反复实例化,控制台会报错。t3code里用了 Next.js 官方推荐的全局存储方式:

import { PrismaClient } from "@prisma/client"; const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient }; export const prisma = globalForPrisma.prisma ?? new PrismaClient(); if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

server/auth.ts里是 NextAuth 的配置项。我适配了 Google 和 GitHub 两种 OAuth 登录,同时保留了一个 credentials 登录的占位实现用于开发环境调试。注意prisma适配器用的是@auth/prisma-adapter,这样才能把 session 和用户数据持久化到数据库里,而不是默认存在内存中。

4.3 服务端组件与客户端的界线怎么执行

在 App Router 模式下,很多新手会把所有页面都标成"use client",这个做法会毁掉服务端渲染带来的优势。t3code的约定是:

  • 尽量不用客户端组件:列表、详情、概览这类纯展示页面全部使用服务端组件。
  • 需要表单交互、用户状态、动态输入的页面局部标记为客户端组件。
  • 客户端组件不直接调用 Prisma,必须通过 tRPC 的 API 层访问数据。

这个约束为什么会出现在样板里?因为实际踩过坑。早期我图省事,在几个页面里直接import { prisma } from "@/server/db"在客户端组件里读写数据库。开发环境一切正常,部署后才发现 Prisma 引擎文件在边缘运行时兼容性问题一大堆。后来干脆在项目约定里写明:客户端组件禁止直接使用server/*下的模块,统一走api客户端。这条红线保住之后,项目结构清晰很多。

5. 认证、权限与类型约束:怎么让“登录后”这个状态成为编译期的一部分

5.1 NextAuth 在t3code里的接入方式

如果你用过 NextAuth,应该知道它的配置改起来小坑不少,尤其是回调函数和 session 策略。t3code里的配置卡片如下:

// server/auth.ts import { type NextAuthOptions } from "next-auth"; import GoogleProvider from "next-auth/providers/google"; import { PrismaAdapter } from "@auth/prisma-adapter"; import { prisma } from "@/server/db"; export const authOptions: NextAuthOptions = { adapter: PrismaAdapter(prisma), session: { strategy: "database" }, providers: [ GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, }), ], callbacks: { session: async ({ session, user }) => { if (session.user) { session.user.id = user.id; } return session; }, }, };

注意这里 session 策略选的是database而不是jwt。因为我们的用户状态存在数据库里,撤销一个 session 直接删库里记录即可,适合后台管理系统这类对安全要求较高的场景。

5.2 Session 注入 tRPC 上下文的细节

前面第三章已经展示了createTRPCContext,这里面有个容易被忽略的坑:NextAuth 的getServerSession是异步的。如果你在一个比较老的 Next.js 版本上用旧的getSession方法,它会额外调接口、性能也差。用 App Router 之后,正确的做法是调用getServerSession(authOptions)直接读取 cookie 并解析。

要把 session 的类型完整声明给 tRPC,我在types/next-auth.d.ts里做了类型扩展:

import { DefaultSession } from "next-auth"; declare module "next-auth" { interface Session { user: { id: string; } & DefaultSession["user"]; } }

这样ctx.session.user.id就有了明确的字符串类型,不会在写代码时被 IDE 提示user.id不存在。

5.3 前端权限与后端权限的双层样板

权限不能只靠前端隐藏入口来保证。t3code的实践是:前端决定“看到什么”,后端决定“能做什么”。

前端侧,我写了一个RequireAuth包裹组件,在服务端组件的layout里调用:

export default async function DashboardLayout({ children }: { children: React.ReactNode }) { const session = await getServerSession(authOptions); if (!session) { redirect("/login"); } return <>{children}</>; }

后端侧,所有需要身份的接口都用protectedProcedure包裹。你不需要在每个 resolver 里重复校验,只需要在创建 router 时决定这个流程走哪个封装。这个设计实际上已经是你未来加 RBAC 的基础:可以扩展出一个adminProcedure,在中间件里同时校验用户是否为管理员。这个思路比在每一个接口里手写条件判断清晰得多,团队协作时也不容易漏。

6. 性能与部署优化:开发环境跑通不等于生产环境能端稳

6.1 为什么我坚持用 pnpm 而不是 npm

在t3code里我选择 pnpm 作为唯一的包管理器。核心原因是它通过硬链接和符号链接大幅减少了依赖占用的磁盘空间和安装时间,更重要的是在 monorepo 场景下,pnpm 的严格 node_modules 结构能提前暴露依赖关系不清晰的问题。如果你用 npm 或者 yarn,可能会遇到一个项目里装了两个不同版本的 React 导致 hooks 状态失效的问题,这类问题在 pnpm 下几乎不会出现,因为它默认隔离了非直接依赖。

6.2 Prisma 在 serverless 环境的连接管理

这个点是我在生产环境踩过最大的坑。开发环境跑得好好的,部署到 Edge/Serverless 平台后,数据库连接疯狂报错——原因很简单:每个函数实例都会创建自己的 Prisma 客户端实例,连接数瞬间打满数据库。t3code里的解决方案有两个部分:

第一,Prisma 客户端用全局单例,减少重复创建。

第二,在 serverless 环境建议直接使用连接池代理,比如 Vercel Postgres 自带的连接池 URL 或者 Prisma Accelerate。我的做法是让DATABASE_URL指向连接池地址,而真正的事务型连接通过配置开启连接限制:

new PrismaClient({ log: ["warn", "error"], datasources: { db: { url: process.env.DIRECT_URL }, }, })

如果你使用 Supabase 或 Neon,这类平台自带兼容 PostgreSQL 的连接池机制,只需要在连接串里带上pool_timeout=0等参数。帮我省掉的最大一个问题就是——同样一套代码在本地和线上运行时,再也不会出现连接风暴。

6.3 构建提速与产物优化

为了减少首屏 JS 体积,我在next.config.mjs里做了一组实测有效的配置:

/** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, transpilePackages: ["@t3code/ui"], swcMinify: true, experimental: { optimizePackageImports: ["@trpc/client", "@trpc/react-query"], }, }; export default nextConfig;

experimental.optimizePackageImports是 Next.js 的按需构建优化选项。它在打包时对特定包做依赖图谱分析,把深层次的引入关系拍平,减少最终产物体积。实测对一个包含 tRPC、React Query 的页面,改造后 gzip 后体积减少了约 7%,感觉不明显,但好处是不需要手动做代码分割的粗糙处理。

还有一点是关于图片和字体的。t3code默认使用next/image,所有图片必须走Image组件,不要直接用<img>。next/image会根据设备尺寸生成不同规格的图片,避免移动端加载 2MB 大图的问题。这个在刚搭建时可能没什么体感,但在真实用户量上来之后,页面性能的差距会变得非常明显。

7. 踩坑实录:t3code在实际项目中遇到的问题清单

7.1 tRPC 服务端调用与 App Router 的响应类型不一致

我把server/trpc/caller.ts封装出来,是为了在 Server Component 里直接调用 tRPC procedure,不用发网络请求:

import { createTRPCContext } from "./trpc"; import { appRouter } from "@/modules"; import { createCallerFactory } from "@trpc/server"; const createCaller = createCallerFactory(appRouter); export async function serverCaller() { const ctx = await createTRPCContext(); return createCaller(ctx); }

但在实际使用中,我发现一个坑:在"use client"组件里绝不能使用serverCaller。因为 App Router 会把客户端组件代码打包到浏览器,而serverCaller内部依赖了node:http相关的东西,直接执行会报错。解决方案是写一个严格的 eslint 规则,禁止在客户端目录下导入server/trpc/caller。这个规则已经躺在t3code的.eslintrc里面。

7.2 Tailwind 的 class 合并导致样式丢失

这是 Tailwind 项目里的经典坑。我用tailwind-merge处理通用组件的 class 合并问题。核心场景是:一个<Button>组件默认是bg-blue-500,外部调用时想覆盖为bg-red-500,直接替换 class 字符串会导致两个类共存,最终样式不可预测。tailwind-merge能识别冲突的 Tailwind 类并保留后传入的那个。这个工具在t3code的lib/utils.ts里有封装:

import { twMerge } from "tailwind-merge"; import { clsx, type ClassValue } from "clsx"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }

现在所有 UI 组件都把cn当作唯一入口来处理 className,组件复用时表现稳定多了。

7.3 Prisma 枚举和 Zod enum 的不同写法导致的数据不一致

在早期版本里,我在schema.prisma里定义了:

enum TaskPriority { LOW MEDIUM HIGH }

同时又在 zod schema 里写:

z.enum(["LOW", "MEDIUM", "HIGH"])

两边是重复维护,一旦改一个忘一个就出问题。后来我用prisma-zod-generator直接从 Prisma schema 生成 zod 枚举类型,舍掉了手工维护这个环节。修改枚举值只改 Prisma 文件,重新prisma generate后类型和校验器一起更新。

7.4 部署环境的 Node 版本不一致

项目里同时用了 Next.js 的outputFileTracing和 Prisma 的 query engine,对 Node 版本比较敏感。t3code在根目录加了.nvmrc和engines字段:

{ "engines": { "node": ">=20.9.0" } }

同时在package.json的scripts里加了 preinstall 钩子检查 node 版本是目前兼容的版本,不行就报错。

现在的很多 PaaS 平台都有一个 Node 版本设置,要么在环境变量里配,要么在vercel.json/fly.toml里写。如果不强制锁定,本地 18、线上 22 这种组合会带来一堆莫名其妙的高版本产物差异。这份配置能避免 90% 的“我在本地是好的”问题。

7.5 什么不该塞进样板工程

最后聊一个反向经验。早期我总想往t3code里加更多东西:i18n、暗黑模式、Storybook、E2E 测试……后来发现项目越来越重,配置越来越复杂,实际业务却还没开始。样板工程的本质不是“全都要”,而是“可复用的最小完整闭环”。

所以我砍掉了孤立的 Storybook,组件文档直接写在组件文件同目录下的.stories.tsx文件里;E2E 测试保留一条 Playwright 冒烟链路;i18n 这种跟业务强相关的功能,等真正需要时再以模块化方式引入。删完之后t3code轻了一大半,维护起来也更舒服了。

8. 根据我的实践给你的三条骨架建议

如果你也想基于这套思路搭自己的工程基线,我给你三条骨架建议,都是动手写代码之后慢慢沉淀出来的:

  1. 类型安全要框架化,不要靠自觉。采用能自动推导类型的方案(tRPC 或者类似的 RPC 框架),并在中间件、上下文、校验器上做足文章。约束一旦成型,整个团队写代码时都会被保护。

  2. 目录按产品模块切,不要按技术分层切。模块自洽是后续拆服务、重组代码最重要的前提。模块内部可以自由组织,模块之间只通过明确的 API 边界通信。

  3. 本地环境与生产环境从第一天起就保持同构。连接池、Node 版本、环境变量都应当通过配置文件固定。这块省下的时间,远高于前期多花的时间。

我在实际使用中发现,t3code真正给我带来的最大价值不是它本身的代码,而是每当我启动一个新项目时,不用再思考“登录怎么做、权限怎么拦、类型怎么传”,可以直接把精力投入到业务逻辑本身。样板工程做到最后,你会发现自己最享受的就是这种不被重复劳动打扰的安静。如果你最近也在折腾全栈工程的基建问题,希望能沿着我复盘的这条路径,踩更少的坑,把真正该花的时间留给业务探索。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询