这一段时间我一直在维护一个内部脚手架项目 t3code。t3code 不是某个高深莫测的框架,而是我把 Next.js、TypeScript、Tailwind CSS、tRPC、Prisma 这些常用技术组合成的一套全栈项目起点模板,同时带了一个命令行工具,能一键生成带认证、数据库、API 层的完整代码骨架。对于需要快速启动业务原型,又不想每次从零手搓登录注册和模型迁移的团队来说,它能省掉大量重复劳动。如果你正准备学习全栈开发,或者想找一个能直接落地改业务的基线,这篇文章应该对你有用。
t3code 这个名字其实很直白:T3 是社区里对“TypeScript 全栈三驾马车”的简称,code 则是代码生成的意思。我在维护它的过程中踩了不少坑,也积累了一些真实能用的操作经验。下面把我做技术选型的逻辑、核心实现细节、完整实操流程和常见排障过程都整理出来,给需要自己搭脚手架或者想用类似方案的同学做一个参考。
1. 项目概述与核心设计思路
1.1 t3code 是什么:从痛点说起
先说痛点。我在团队里经常要做一些内部工具和小型业务系统,比如报销审批、活动报名、数据看板。这些东西功能不复杂,但每次都逃不掉几个固定步骤:初始化一个 Next.js 项目,接上 TypeScript 配置,搭 Tailwind 样式,设计数据库表,写一堆登录注册和 Session 逻辑,再手动创建 REST API 路由。一个项目光做这些准备工作,最快也得半天,如果中间遇到版本兼容问题,时间还会翻倍。
t3code 就是把这一整套“地基”沉淀成模板。它不是要你手写代码,而是通过交互式命令行,让你选择需要的模块,比如要不要 NextAuth 认证、要不要 Prisma 数据库层、要不要 tRPC 接口层。选完之后它会直接生成一个可以跑起来的全栈项目,自带当前用户的登录注册、数据库模型、API 路由和前端页面。生成的代码不是死模板,而是带完整类型推导的活代码,改起来非常顺畅。
和官方 create-t3-app 相比,t3code 更像是一个持续演进的“业务脚手架合集”。create-t3-app 生成的是最基础的 T3 模板,而 t3code 会预置一些更贴近真实业务的模块,比如多角色权限、软删除字段、统一错误返回格式、操作日志记录。我从实际项目里抽出来这些通用能力,让新项目一开始就站在一个相对完整的基线上,而不是只得到一个空壳。
1.2 为什么选择 T3 技术栈:选型背后的逻辑
很多人问我为什么要押注 T3 技术栈,而不是用 Spring Boot 或者直接写 REST API。我的理由其实很实际:团队小、节奏快、需要类型安全。
T3 技术栈的核心是“全栈 TypeScript”。前端用 Next.js 和 React,后端用 tRPC 和 Prisma,整个链路里,数据库模型定义在 Prisma Schema 里,API 层由 tRPC Router 暴露,前端组件直接调用生成的类型安全函数。这意味着如果你改了数据库字段,TypeScript 会立刻告诉你有哪个前端页面引用了这个字段,根本不需要靠 Postman 或文档去维护一套接口契约。
tRPC 这个选择是关键。REST API 看起来简单,但要维护 OpenAPI 规范、版本号和客户端 SDK,对中小项目来说是额外负担。tRPC 不定义 URL,而是让前端直接“调用”服务端函数,像写本地函数一样写接口。这种模型特别适合内部系统:数据模型一致、改动成本低、排查问题方便。当然,如果未来要做开放的第三方 API 或服务很多端,我仍然建议用 REST 或 GraphQL,t3code 在这点上没有绑定死,删掉 tRPC 模块也能用 Next.js Route Handler 写普通接口。
数据库层选择 Prisma 也很顺理成章。Prisma Schema 是单一事实来源,团队评审模型时只看一个文件就行;migration 文件可以进版本库,部署前自动执行;生成出来的 Client 自带类型,查询时不会有手写 SQL 的字符串安全隐患。我用过 TypeORM 和 Sequelize,但 Prisma 在开发体验上最省心。
1.3 项目结构总览与目录规划
t3code 生成的项目目录结构大致如下:
my-app/ ├── .env.example ├── .env ├── package.json ├── prisma/ │ ├── schema.prisma │ └── seed.ts ├── public/ │ └── favicon.ico ├── src/ │ ├── app/ │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── dashboard/ │ │ └── page.tsx │ ├── components/ │ │ ├── ui/ │ │ └── layout/ │ ├── server/ │ │ ├── api/ │ │ │ ├── root.ts │ │ │ └── routers/ │ │ │ ├── auth.ts │ │ │ └── user.ts │ │ ├── auth.ts │ │ └── db.ts │ ├── trpc/ │ │ ├── server.ts │ │ └── client.tsx │ ├── middleware.ts │ └── styles/ │ └── globals.css └── tailwind.config.ts这个结构和 create-t3-app 的早期版本很像,但我在server/api/routers里加了更多通用业务模块。src/server/db.ts只管创建 Prisma Client,src/server/auth.ts封装 NextAuth 配置,src/trpc/server.ts负责创建 tRPC 实例和鉴权中间件。前端调用时通过src/trpc/client.tsx中的 Provider 包裹整个应用。
这样的目录承载能力比较强:几乎每个业务实体都可以在routers里建一个独立文件,然后挂载到root.ts。不至于做一个新功能就要重构整个项目结构。
2. 核心功能实现与实操细节
2.1 环境准备:Node 版本与依赖安装
t3code 生成的项目对 Node 版本有要求,最低需要 Node 18.17,推荐用 Node 20 LTS。因为 Prisma 和 Next.js 的生态都在往这个版本靠,Npm 包原生模块也大多需要较新的 Node 支持。如果你本机没有切换过 Node 版本,建议先装 nvm,然后执行:
nvm install 20 nvm use 20依赖安装我推荐 pnpm。不是所有项目都适合 pnpm,但全栈 TypeScript 项目用 pnpm 可以明显减少 node_modules 占用,同时避免幽灵依赖带来的问题。t3code 的模板里已经准备好了pnpm-lock.yaml,你只需要运行:
pnpm install这里有个容易踩的坑:pnpm 的 fetch 速度和网络环境关系很大,部分沙箱或企业网络会自动忽略.npmrc里的 registry 配置。如果你遇到安装中途超时,可以在项目根目录创建.npmrc文件,把镜像源地址写进去。注意镜像源只影响 npm 包下载,不影响业务代码运行。
2.2 数据库层:Prisma 模型设计与初始化
进入项目的第一件事通常是调整数据库模型。t3code 默认会生成一个最小可用的schema.prisma,包含用户和会话相关模型。业务模型的扩展一般从加表开始,比如我们要做“项目任务管理”,可以这样设计:
model User { id String @id @default(cuid()) email String? @unique name String? passwordHash String? projects Project[] tasks Task[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Project { id String @id @default(cuid()) name String ownerId String owner User @relation(fields: [ownerId], references: [id]) tasks Task[] createdAt DateTime @default(now()) } model Task { id String @id @default(cuid()) title String done Boolean @default(false) projectId String project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) assigneeId String? assignee User? @relation(fields: [assigneeId], references: [id]) createdAt DateTime @default(now()) }这里有几个设计细节要重点说明。首先,主键全部用cuid()而不是自增 id,这样可以避免暴露数据量,也方便将来做多库分区。其次,Task.Project的onDelete: Cascade表示删除项目时会连带删除任务,这种约束放到数据库层比放到业务代码里更可靠。第三,User和Task之间的assigneeId是可选的,因为任务可能还没分配人。
模型写完保存后,执行迁移:
npx prisma migrate dev --name init_project_task npx prisma generatemigrate dev会创建 SQL 迁移文件并同步到开发数据库。t3code 模板默认开发库是 SQLite,方便本地启动;生产环境可以把DATABASE_URL改成 PostgreSQL,只要 Prisma Schema 里的 provider 换成postgresql就好。这个切换很简单,因为 Prisma 会为不同数据库生成对应的 SQL 语法。
2.3 服务端逻辑:tRPC 路由的写法与鉴权
tRPC 的核心理念是让服务端函数被前端直接调用。t3code 里的 tRPC 实例是一个叫t的对象,它负责创建 router 和 procedure。每个需要登录才能访问的接口,都应该用protectedProcedure而不是publicProcedure。一个典型的路由文件长这样:
// src/server/api/routers/project.ts import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "~/trpc/server"; import { prisma } from "~/server/db"; export const projectRouter = createTRPCRouter({ listMyProjects: protectedProcedure .query(async ({ ctx }) => { return prisma.project.findMany({ where: { ownerId: ctx.session.user.id }, include: { tasks: true }, }); }), create: protectedProcedure .input( z.object({ name: z.string().min(1).max(100), }) ) .mutation(async ({ ctx, input }) => { return prisma.project.create({ data: { name: input.name, ownerId: ctx.session.user.id, }, }); }), });protectedProcedure是 t3code 里最重要的封装。在src/trpc/server.ts中,我会写一个中间件,先判断ctx.session是否存在,不存在就抛错。这样前端收到的错误信息会统一走 tRPC 的 error 格式,用TRPCClientError就能捕获。很多新手容易把 session 判断写在每个路由里,那样代码重复量太高,后续改鉴权方案也麻烦。
把 server 端 router 挂载到根节点:
// src/server/api/root.ts import { projectRouter } from "./routers/project"; import { createTRPCRouter } from "~/trpc/server"; export const appRouter = createTRPCRouter({ project: projectRouter, user: userRouter, });这样前端调用时就是trpc.project.listMyProjects.useQuery(),层次清晰,也不容易和其他模块冲突。
2.4 前端页面与 Tailwind 样式的联动
前端部分,t3code 使用 Next.js App Router。页面文件放在src/app下,比如src/app/dashboard/page.tsx是登录后的默认页。一个调用 tRPC 的页面组件可以这样写:
"use client"; import { trpc } from "~/trpc/client"; export default function DashboardPage() { const { data: projects, isLoading, refetch } = trpc.project.listMyProjects.useQuery(); if (isLoading) { return <div className="p-8 text-sm text-gray-500">加载中...</div>; } return ( <div className="mx-auto max-w-5xl p-8"> <h1 className="text-2xl font-bold">我的项目</h1> <div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"> {projects?.map((project) => ( <div key={project.id} className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm"> <p className="font-medium">{project.name}</p> <p className="text-sm text-gray-500">{project.tasks.length} 个任务</p> </div> ))} </div> </div> ); }这段代码里有几个关键点。第一,组件开头必须加"use client",因为useQuery依赖 React hooks,不能直接在 Server Component 里使用。第二,trpc.project.listMyProjects这个方法的返回类型直接由 server 端推导,所以project.name如果打错字,编辑器会立刻报错。第三,Tailwind 的响应式类名直接写在 JSX 里,比单独维护 CSS 文件要直观。
如果希望在服务端组件里直接拉数据,t3code 也提供了trpc.server调用的方式,可以在不需要交互的页面里预取数据。不过要特别注意,服务端调用会把整个 prisma client 打包到 server chunk 里,不要把它误用为客户端组件的数据源。
3. 完整实操流程:从零到部署
3.1 5分钟快速初始化 t3code 项目
假设你本地环境已经装好 Node 20 和 pnpm,初始化一个新项目只需要几步:
npx t3code init my-app cd my-app cp .env.example .env pnpm install pnpm prisma:migrate pnpm prisma:seed pnpm devnpx t3code init会进入交互界面,让你选择需要包含的模块。如果你是第一次试用,建议全选默认模块:NextAuth、Prisma、tRPC、Tailwind、ESLint。这样能获得最完整的体验。
执行pnpm prisma:migrate时,模板内置脚本会自动跑prisma migrate dev并生成客户端。如果数据库还没有创建,Prisma 会自动创建 SQLite 文件,这个过程不需要额外配置环境变量。启动pnpm dev后,浏览器打开http://localhost:3000,你会看到一个登录页面。注册一个新用户,系统会跳转到/dashboard并显示刚才创建的默认空项目。
这套流程我实测过很多次,大多数问题都出在pnpm install那一步,比如 node_modules 安装不完整导致 Prisma Client 未生成。遇到这个情况,直接执行npx prisma generate,再重启 dev server 就能解决。
3.2 新增一个业务模块的完整流程
从一个运行中的 t3code 项目出发,新增业务模块的核心步骤我总结为四步:改模型、生成迁移、建路由、写页面。拿“用户收藏项目”功能举例。
第一步,在prisma/schema.prisma中增加 Favorite 模型:
model Favorite { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id]) projectId String project Project @relation(fields: [projectId], references: [id]) createdAt DateTime @default(now()) @@unique([userId, projectId]) }第二步,执行npx prisma migrate dev --name add_favorite。这里会提示生成了一个新的迁移文件,同时在数据库里创建 favorite 表。
第三步,在src/server/api/routers/favorite.ts写两个端点:一个用来查询当前用户的收藏列表,一个用来切换收藏状态。注意要放到 protectedProcedure 下,并且查询时用where: { userId: ctx.session.user.id }做隔离,避免越权访问。
第四步,在前端新增一个“收藏”按钮组件,调用trpc.favorite.toggle.useMutation()。当 mutation 成功后,用refetch()刷新列表。这四个步骤加起来大概半小时,其中一半时间都在等数据库迁移和类型检查。
这个流程之所以顺畅,核心原因是 t3code 已经帮你把鉴权、数据库连接、错误边界都处理好了,你只需要关注业务逻辑本身。我在初始化项目时最大的建议是:先让“增删改查”跑通,再去考虑复杂的权限策略和服务拆分。
3.3 构建、测试与部署的注意事项
本地开发完成之后,进入构建和部署阶段,有三件事要格外留意。
首先是环境变量管理。.env.example中会列出所有必需的环境变量:DATABASE_URL、NEXTAUTH_SECRET、NEXTAUTH_URL。在 CI 或服务器上,绝不能缺少这些变量。如果部署到 Vercel,需要在项目配置里逐个添加,而且NEXTAUTH_URL必须设置为线上域名,不能用http://localhost:3000。
其次是 Prisma 的构建钩子。在package.json里,我建议加上这段脚本:
{ "scripts": { "postinstall": "prisma generate", "build": "next build" } }postinstall会在依赖安装完成后自动生成 Prisma Client。如果不加,云端构建时很可能会因为找不到@prisma/client的类型而报错。部署前还要先执行prisma migrate deploy,这个命令会按顺序执行迁移文件,但不会创建自己的开发迁移。生产环境建议把prisma migrate deploy放在构建前或启动前执行,避免应用启动时才产生现场变更。
第三,Docker 部署时要注意 Prisma 引擎二进制文件的大小。Prisma 在容器里需要下载对应的 query engine,如果基础镜像没有安装 openssl,就会在运行时打不开数据库连接。通常我会在 Dockerfile 里加上:
RUN apt-get update && apt-get install -y openssl ca-certificates这样可以免去很多无谓的排查时间。
4. 常见问题与排查技巧实录
4.1 排查 tRPC 类型报错的心得
使用 tRPC 最舒服的是类型自动推导,但一旦加了新路由后前端没有吃到新类型,最常见的表现是页面一直报 TS 错误,或者某个 API 方法不存在。
我通常先做三步排查。第一步,重启next dev进程,因为 Development Server 偶尔不会热更新到 tRPC 的类型。第二步,删除.next目录再重启,这个操作能清掉旧的类型缓存。第三步,检查src/server/api/root.ts里是否真的把新 router 挂载到了 appRouter,有时候只写了 router 文件却忘了挂载,前端自然找不到对应的调用入口。
还有一个容易被忽略的点:tRPC v10 和 v11 的 API 稍有不同。t3code 目前锁定 v10 的稳定版本,如果你手动升级到 v11,createCallerFactory和 middleware 写法会有变化。升级前一定要读迁移文档,否则会出现一堆编译错误。
4.2 Prisma migrate 与数据库连接问题
Prisma 相关的数据库连接错误,大多数时候不是 Prisma 本身的问题,而是环境变量或网络配置的问题。
最典型的是Can't reach database server。排查顺序应该是:先看.env里DATABASE_URL是否填写正确;然后确认本机能否 ping 通localhost;再检查数据库端口是否被别的进程占用。如果你用的是远程 PostgreSQL,还要注意数据库主机是否允许当前 IP 的访问。
另一个常见问题是schema.prisma中的 provider 和DATABASE_URL前缀不一致。比如 schema 写成provider = "postgresql",但 URL 写的是file:./dev.db,Prisma 会直接报错。这个我认为应该放在 checklist 的第一条:改 provider 之前,先确认 URL 前缀对称。
如果遇到P2002这种 unique 约束错误,通常是因为重复插入了相同唯一键的数据。t3code 模板里的注册流程会检查邮箱是否已存在,但如果你直接操作数据库,或者 seed 脚本跑了多遍,就可能撞上这个错误。处理方式是先清理脏数据,再重新 seed。
4.3 Next.js 路由与中间件的坑
App Router 是 t3code 的默认模式,它比 Pages Router 更简洁,但坑也更隐蔽。最容易出问题的是“客户端组件与服务端组件混用”。比如你在layout.tsx里使用了SessionProvider,那这个 layout 必须被标记为客户端组件,否则 React Context 无法在服务端渲染时使用。而这又意味着整个 layout 子树都会被拉到客户端 bundle 里,体积会变大。
更细的问题是 middleware 中的 matcher 配置。t3code 里有一个src/middleware.ts用来检查未登录用户跳转,我通常会匹配除静态资源和登录注册页之外的所有路径:
export const config = { matcher: ["/((?!api|_next/static|_next/image|favicon.ico|login|register).*)"], };这段正则如果写错,会导致登录页也被中间件重定向,形成无限循环。所以修改 matcher 时,一定要带着真实环境测试。
还有一个坑是路径别名。模板里配置了@/*指向src/*,但如果你在tsconfig.json里改动了baseUrl,忽略给@/server/db加引用,运行时会出现Cannot find module '@/server/db'。这类问题最容易在 IDE 不报错、但线上构建失败的时候出现,我建议修改路径配置后马上跑一次next build。
4.4 性能与打包体积的优化建议
t3code 生成的项目默认够用,但不意味着不用优化。对一个内部管理系统来说,我最先看的是首屏 JS 体积。使用@next/bundle-analyzer可以直观看到每个页面打包了哪些依赖。
如果发现 prisma client 被打包到了前端,几乎可以肯定是错误引用。比如在客户端组件里import { prisma } from "~/server/db",这就把整个 server 依赖打包进去了。正确的做法是:只有 tRPC server 相关文件可以引用 db,客户端只能通过 tRPC 调用数据。
另一个优化点是 Tailwind 的类名清理。虽然 Tailwind 本身会摇树,但如果你使用了动态拼接类名,比如color-${color},编译后的 CSS 可能不包含变色类。我的习惯是所有颜色直接用完整类名,不做字符串模板拼接。
字体和图片优化也很关键。Next.js 的next/font可以自动做字体子集化,但默认加载的 Google 字体在服务器不稳定的地区会成为首屏瓶颈。t3code 模板里没有强绑外部字体,如果需要,我建议下载到本地再通过next/font/local引入。
5. 使用经验与后续扩展
5.1 我个人维护 t3code 的几点体会
维护 t3code 这一年多,最深的体会是“脚手架工具要克制”。一开始我什么都想往里塞,比如文件上传、微信登录、消息通知,但后来发现模板里模块太多,新用户反而不知道从哪下手,维护成本也指数级上升。后来我立了一个规矩:只有至少被三个真实项目复用过且不需要大量定制的能力,才允许进入默认模板。这个思路让 t3code 一直保持在一个相对精简的状态。
另外,文档一定要和版本同步。早期我经常改了生成逻辑却没有更新 README,结果用户在 GitHub Issues 里问的问题与代码长期对不上,既浪费时间也影响信任。现在我在发版前会跑一遍从npx t3code init到部署的完整流程,按真实输出截图写文档。这个流程比较笨,但确实避免了大量无效沟通。
5.2 后续计划:插件化与模板交换
t3code 现在还是一个“单模板”工具,用户只能在我预设的模块集合里做选择。但业务场景差异很大,未来我打算把它改成插件化的设计:核心只负责项目骨架和依赖管理,业务模块通过模板 package 动态加载。到时候用户可以自己写一个“blog”插件,也可以通过社区直接安装别人维护的“支付套餐”。
同时,我还想增加一个“导入旧项目”的命令。很多团队手里有传统的 Express 项目,想把数据层和认证逐步迁移到 t3code 上。如果有一个工具能把现有 Prisma Schema 和 NextAuth 配置解析成 t3code 的目录结构,迁移门槛会低很多。这个功能我正在尝试做,不过还没有到可以公开使用的阶段。
如果你也在做类似的脚手架,或者希望 t3code 支持某个具体业务模块,可以在 GitHub 上给我提 issue。我个人在实际使用中发现,一个小型全栈脚手架如果打磨得足够贴心,带来的长期收益比想象中大得多。最后分享一个小技巧:不要追求一上来就“完美的架构”,先把可运行的东西放到面前,让团队尽早进入业务逻辑,这比任何炫技都重要。