Tanstack Start
明白,我们彻底抛弃传统的后端类比,完全用现代全栈 TypeScript 的视角来拆解这个技术栈。
你正在看的这个 Starter 项目(TanStack Start + SQLite)非常轻量且纯粹。在这个生态里,重点是端到端的类型安全和最小化样板代码。
以下是这些库的具体作用,以及它们在一个 SSR(服务端渲染)项目中的扮演的角色:
1. better-sqlite3
- 它是做什么的: 一个专门用于 Node.js 的 SQLite 数据库驱动程序。它的特点是同步执行,并且性能极高。
- 在 SSR 项目中的作用: 它是整个项目与物理数据库文件(
.db文件)交互的“底座”。当你在服务端渲染页面,需要从数据库读取列表时,最终就是靠它将 SQL 指令发送给 SQLite 引擎并拿回数据。在本地开发或轻量级全栈应用中,它免去了配置独立数据库服务器的麻烦。
2. drizzle-orm
- 它是做什么的: 一个极简的、TypeScript 原生的 ORM(对象关系映射)工具。
- 在 SSR 项目中的作用: 它让你用强类型的 TypeScript 代码来替代容易拼错的裸写 SQL 字符串。在 SSR 阶段获取首屏数据,或者在处理表单提交时,你调用 Drizzle 的 API(例如
db.select().from(users)),它会自动将其翻译成 SQL 交给better-sqlite3执行。它的核心优势是:如果你的表结构变了,你的 TypeScript 代码会直接报错,在编译阶段就帮你拦住 Bug。
3. zod
- 它是做什么的: 一个极其强大的 TypeScript 优先的数据校验和模式(Schema)声明库。
- 在 SSR 项目中的作用: 它是服务端的“安全门卫”。在 SSR 架构中,前端组件可以直接调用 Server Functions(服务端函数)来提交数据。Zod 负责在这个函数的第一行拦截请求,验证传入的 JSON 数据格式是否合法(比如:邮箱格式对不对、密码长度够不够)。只有 Zod 校验通过的数据,才会被放行给数据库,防止脏数据注入。
4. drizzle-zod
- 它是做什么的: 一座连接
drizzle-orm和zod的桥梁。 - 在 SSR 项目中的作用: 消除重复劳动。通常你需要写两遍数据定义:一遍在数据库建表,另一遍在验证前端请求。有了
drizzle-zod,你可以基于 Drizzle 的数据库 Schema 自动生成 Zod 的验证 Schema。比如你在数据库定义了email字段是字符串且必填,它就会自动生成一个对应的 Zod 规则,保持前后端数据校验的绝对同步。
5. nitro
- 它是做什么的: 一个极其轻量、跨平台的现代 Web 服务器引擎。
- 在 SSR 项目中的作用: 这是 TanStack Start 运转的心脏。当你启动项目时,真正在监听 3000 端口处理 HTTP 请求的就是 Nitro。当用户访问一个 URL 时,Nitro 接收请求,触发 React 在服务端执行(SSR),将生成的 HTML 字符串发回给浏览器,同时它也负责处理所有的 API 路由和打包部署。它的强大之处在于,它可以让你同一套代码无缝部署到 Node.js 服务器、Vercel、Cloudflare 等不同的云环境中。
6. @ngneat/falso
- 它是做什么的: 一个生成假数据(Mock Data)的现代工具库(类似于以前的 Faker.js)。
- 在 SSR 项目中的作用: 主要用于开发和测试阶段。当你刚搭好数据库,UI 界面还空空如也时,你可以写一个简单的
seed.ts脚本,用 Falso 生成几百条逼真的用户名、头像、文章内容等测试数据填入数据库。这样你就能在 SSR 页面上直接调试真实的数据加载效果,而不需要手动造数据。
总结:它们是如何协同完成一次请求的?
想象一个用户在你的应用中提交了一个“添加任务”的表单:
- 用户操作:点击提交按钮。
- 网络层(Nitro):接收到这个 POST 请求,将其路由到对应的 Server Function。
- 校验层(zod + drizzle-zod):检查用户输入的内容是否为空、是否符合我们在数据库定义的格式。
- 持久层(drizzle-orm + better-sqlite3):校验通过后,将数据转换为 SQL
INSERT语句,同步写入本地的 SQLite 数据库。 - 渲染层(SSR):数据库更新后,TanStack Start 触发重新渲染,去数据库抓取最新的列表生成 HTML 传回给浏览器。
使用 better-sqlite3
整体项目是从 https://github.com/itswil/starter-tanstack-start-sqlite 修改得到
默认情况下我 Win11 的电脑,node v22.20 无法正常使用 better-sqlite3,会遇到
最后的通过重新构建 better-sqlite3 解决了
cd node_modules/better-sqlite3
pnpm run build-release
使用 Tanstack Start 创建一个 RESTful api
参考 https://www.answeroverflow.com/m/1451157676997345300 得知 tanstack start 最新 api 设计
import { createFileRoute } from '@tanstack/react-router'
import { getAllUsers } from '../../modules/users/users.service.server'
export const Route = createFileRoute('/api/user')({
server: {
handlers: {
GET: async ({ request }) => {
const users = await getAllUsers()
console.log(users);
return new Response(JSON.stringify(users), {
status: 200,
headers: {
'Content-Type': 'application/json',
// 可选:如果是外部应用访问,可能需要配置 CORS 相关的 Headers
'Access-Control-Allow-Origin': '*',
},
})
},
},
},
})
要设计一套能在复杂业务场景下稳定运行的 RESTful API,不能只在 createFileRoute 的 handler 里堆砌代码。你需要从分层架构、一致性约定、安全性、以及资源建模四个维度来构建。
以下是基于 TanStack Start + Drizzle + Zod 的成熟 API 设计方案:
1. 目录架构:控制器与服务分离
不要把数据库逻辑写在路由文件里。路由文件(routes/api/...)应该只充当 Controller(控制器),负责收发请求,而 Service(服务层) 负责核心业务。
app/
├── services/ # 业务逻辑层 (纯粹的数据库和业务运算)
│ ├── user.service.ts
│ └── auth.service.ts
├── lib/ # 工具类
│ ├── api-response.ts # 统一响应封装
│ └── exceptions.ts # 自定义异常类
└── routes/
└── api/
├── users.ts # 对应 /api/users (复数形式)
└── users.$id.ts # 对应 /api/users/123 (动态路径)
2. 统一响应与异常处理模型
在复杂场景下,Android 等客户端需要极度稳定的数据结构。建议封装一个 ApiResponse 工具。
app/lib/api-response.ts
export class ApiResponse {
static success(data: any, status = 200) {
return new Response(JSON.stringify({
success: true,
data,
timestamp: new Date().toISOString()
}), {
status,
headers: { 'Content-Type': 'application/json' }
});
}
static error(message: string, code = "INTERNAL_ERROR", status = 500) {
return new Response(JSON.stringify({
success: false,
error: { code, message },
timestamp: new Date().toISOString()
}), {
status,
headers: { 'Content-Type': 'application/json' }
});
}
}
3. 资源建模:复数形式与嵌套
成熟的 RESTful API 遵循 “资源” 导向。
路径设计规范
- 获取用户列表:
GET /api/users - 创建用户:
POST /api/users - 获取特定用户:
GET /api/users/$id - 更新用户:
PATCH /api/users/$id(局部更新用 PATCH,整体替换用 PUT) - 删除用户:
DELETE /api/users/$id
动态路由示例 (app/routes/api/users.$id.ts)
export const Route = createFileRoute('/api/users/$id')({
server: {
handlers: {
GET: async ({ params }) => {
const { id } = params; // 这里的 id 对应文件名中的 $id
const user = await UserService.getById(id);
if (!user) return ApiResponse.error("用户不存在", "NOT_FOUND", 404);
return ApiResponse.success(user);
},
PATCH: async ({ request, params }) => {
const body = await request.json();
// 局部更新逻辑...
return ApiResponse.success({ updated: params.id });
}
}
}
})
4. 复杂业务场景的进阶处理
A. 批量操作与分页 (Pagination)
不要一次性返回所有数据。为 GET /api/users 设计标准的过滤和分页。
// GET /api/users?page=1&limit=20&role=admin
GET: async ({ request }) => {
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get('page')) || 1;
const limit = Number(searchParams.get('limit')) || 10;
const role = searchParams.get('role');
const result = await UserService.list({ page, limit, role });
return ApiResponse.success(result); // result 包含 data 列表和 total 总数
}
B. 认证与权限 (Authentication/RBAC)
Android 端通常使用 Authorization: Bearer <Token>。由于 TanStack Start 目前没有复杂的中间件,你需要在 Service 层或 Handler 开头进行拦截。
const authHelper = (request: Request) => {
const token = request.headers.get('Authorization')?.split(' ')[1];
if (!token || token !== "expected_token") {
throw new Error("UNAUTHORIZED");
}
};
// 在 Handler 中使用
POST: async ({ request }) => {
try {
authHelper(request); // 简单的拦截拦截
// ...逻辑
} catch (e) {
return ApiResponse.error("未授权", "UNAUTHORIZED", 401);
}
}
C. 事务处理 (Transactions)
在复杂业务中(如:创建用户的同时要发放优惠券、创建初始日志),必须使用数据库事务。
app/services/user.service.ts
export async function createFullUser(data: any) {
return await db.transaction(async (tx) => {
const [newUser] = await tx.insert(usersTable).values(data).returning();
await tx.insert(logsTable).values({ action: 'create', userId: newUser.id });
// 如果其中一步失败,整个事务都会回滚
return newUser;
});
}
5. 开发建议总结
- 版本控制: 虽然现在是
/api/user,但为了长远考虑,建议目录改为app/routes/api/v1/users.ts。这样当你的 API 发生重大破坏性变更时,可以发布v2而不影响旧的 Android 客户端。 - 状态码要准确:
- 201: 已创建 (POST 成功)
- 204: 无内容 (DELETE 成功)
- 409: 冲突 (唯一键冲突)
- 422: 语义错误 (Zod 校验失败)
- 安全性: 在生产环境下,确保 API 路由开启了 CORS 限制(通过
app.config.ts中的nitro配置),只允许特定的域名或移动端 User-Agent。
通过这种 Controller -> Service -> DB 的分层结构,即便以后你要从 SQLite 迁移到 PostgreSQL,或者业务逻辑变得极其复杂,你的 API 结构依然能保持清晰。
这是一个非常敏锐的观察。你之所以找不到 index.html 和 main.ts,是因为 TanStack Start 属于元框架(Meta-Framework)。
在传统的 Vite SPA 项目中,你是“手动挡”:你需要自己写 index.html 并手动挂载 React。而在 TanStack Start 中,它是“自动挡”:它通过 Vinxi(底层引擎)和 Vite 插件 自动生成了虚拟的入口点。
以下是它的启动逻辑拆解:
1. 谁取代了 index.html?—— __root.tsx
在 SSR 模式下,HTML 不再是一个静态文件,而是由服务器动态生成的。
- 传统项目:Vite 读取
index.html。 - TanStack Start:它读取
src/routes/__root.tsx。
你会发现 __root.tsx 里通常包含了 <html>、<head> 和 <body> 标签。这个文件就是你的 HTML 模版。当你访问页面时,Nitro 服务器会执行这个组件,将其渲染成 HTML 字符串发送给浏览器。
2. 谁取代了 main.ts?—— 虚拟入口 (Virtual Entry)
查看你的 vite.config.ts:
plugins: [tanstackStart(), nitro()]
这两个插件在起作用:
tanstackStart():它会告诉 Vite:“嘿,别找main.ts了。我会根据用户的src/routes目录自动生成一个客户端入口和服务器入口。”nitro():它负责启动底层的 Web 服务器,处理 API 路由(比如你写的/api/user)和页面渲染请求。
虽然在磁盘上你看不到,但在开发模式下,Vite 内存中存在类似 virtual:tanstack-start-client-entry 的文件,它负责执行 ReactDOM.hydrateRoot()。
3. 项目的启动流程 (The Boot Sequence)
当你运行 pnpm dev 时,发生了以下事情:
- 扫描路由:
tanstack-router扫描src/routes目录,并自动生成src/routeTree.gen.ts。这个文件就是你项目的“地图”。 - 启动 Nitro 服务器:Nitro 启动并接管 3000 端口。
- 路由匹配:当你访问
/时,服务器查看routeTree.gen.ts,发现应该渲染src/routes/index.tsx。 - 渲染层级:
- 服务器先渲染
__root.tsx(外壳)。 - 然后在
__root.tsx的<Outlet/>位置渲染index.tsx(内容)。
- 注水 (Hydration):服务器把生成的 HTML 发给浏览器,同时附带一个隐藏的脚本。浏览器加载这个脚本后,React 接管页面,使其变得可交互。
4. 为什么你在 src/routes/index.tsx 就能跑起来?
因为 TanStack Start 采用了约定优于配置 (Convention over Configuration):
- 入口路由:
src/routes/index.tsx自动映射到/。 - 根路由:
src/routes/__root.tsx自动作为所有页面的父级。 - 路由实例:
src/router.tsx里的getRouter函数定义了路由的行为(如 404 页面、滚动恢复等),框架会自动调用它。
总结对照表
| 功能 | 传统 Vite SPA | TanStack Start SSR |
|---|---|---|
| HTML 结构 | index.html (静态文件) |
src/routes/__root.tsx (React 组件) |
| JS 入口 | src/main.ts |
框架自动生成的虚拟客户端入口 |
| 路由定义 | 手动配置 react-router-dom |
src/routes/ 目录结构 + routeTree.gen |
| 服务器 | 无 (或简单的开发服务器) | Nitro (全功能生产级服务器) |
这种设计的初衷是: 让你专注于写业务(路由和组件),而把复杂的 SSR 配置(如何脱水、如何注水、如何生成 HTML)全部交给框架层处理。
你现在可以去看看 src/routes/__root.tsx,你会发现那里就是你曾经在 index.html 里写的那些东西。
在 TanStack Start 这个极其现代(且还在快速迭代)的元框架中,传统的 Express/Koa 式的 app.use() 中间件思维需要发生一些转变。
在 TanStack Start 的架构下,中间件(特别是像登录鉴权这样的逻辑)通常被拆分设计在三个不同的层级。同时,市面上确实有非常契合这套 Drizzle + Zod 栈的现代 Auth 框架。
以下是具体的架构设计方案和框架推荐:
一、 鉴权中间件的三层架构设计
在 TanStack Start 中,鉴权不只是在一个地方拦截,而是分为路由层、Server Function 层和原生 API 层:
1. 路由层拦截 (UI 保护)
这是保护前端页面不被未登录用户看到的防线。TanStack Router 提供了强大的 beforeLoad 钩子和 Context 机制。
设计思路:
在 __root.tsx 中注入全局的 Auth 状态,然后在需要保护的路由(如 /dashboard)中进行拦截。
// src/routes/dashboard.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/dashboard')({
// beforeLoad 在路由加载前执行,支持服务端和客户端
beforeLoad: async ({ context }) => {
// 假设 context.auth 是你在 root 注入的鉴权状态
if (!context.auth.isAuthenticated) {
throw redirect({
to: '/login',
search: { redirect: '/dashboard' }, // 记录原本想去的页面
})
}
},
})
2. Server Function 层拦截 (RPC 数据保护)
虽然路由拦截了 UI,但恶意用户依然可以直接请求获取数据的接口。TanStack Start 专门为此设计了 createMiddleware API。
设计思路:
创建一个可复用的 Auth 中间件,应用到所有需要保护的服务端函数上。
// src/middleware/auth.ts
import { createMiddleware } from '@tanstack/start'
import { getSession } from '@/lib/auth'
export const authMiddleware = createMiddleware()
.server(async ({ next }) => {
// 1. 获取 Cookie / Token 并验证
const session = await getSession()
if (!session) {
throw new Error('Unauthorized') // 抛出异常阻断执行
}
// 2. 将用户信息注入到下游上下文中
return next({
context: { user: session.user }
})
})
// ------------------------------------
// src/server/user-actions.ts
import { createServerFn } from '@tanstack/start'
import { authMiddleware } from '@/middleware/auth'
export const updateProfile = createServerFn('POST')
// 3. 使用中间件
.middleware([authMiddleware])
.handler(async ({ context, data }) => {
// 这里绝对安全,context.user 一定存在
const userId = context.user.id
// ... 执行 Drizzle 数据库更新
})
3. Nitro 层拦截 (纯粹的 API 路由保护)
如果你对外暴露了供第三方调用的 RESTful API(位于 app/routes/api/),你需要利用底层的 Nitro 引擎中间件。在项目中创建一个 server/middleware/ 目录,Nitro 会自动加载它们来拦截所有原生 HTTP 请求。
二、 市面上成熟的鉴权框架推荐
对于 React + Drizzle + Zod 这个现代化栈,不推荐使用过于老旧或强绑定特定框架(如 Passport.js 甚至早期的 NextAuth)的库。
目前市面上有两个绝对的主流推荐,它们都是框架无关(Framework-Agnostic)且对 TypeScript 和 Drizzle 支持极佳的:
🥇 1. Better Auth (当前最强烈推荐)
这是目前 TypeScript 全栈圈子里最火的 Auth 框架,完美契合你的技术栈。
- 为什么推荐:
- 原生支持 Drizzle:只需一行代码配置,它就会自动为你生成和管理 Users, Sessions, Accounts 等表。
- 自带 Zod 支持:非常契合你现有的 Zod 生态。
- 功能极其丰富:开箱即用支持 OAuth(GitHub, Google 等)、Magic Links、2FA(双因素认证)、Passkeys,甚至自带多租户(Organization)支持。
- 框架无关:它提供了一个统一的实例,无论是给 TanStack Start 的 Server Function 用,还是给 React 的 Hook 用,都极其丝滑。
Better Auth 结合 Drizzle 的初始化示例:
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "./db"; // 你的 Drizzle 实例
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg", // 或 sqlite, mysql
}),
emailAndPassword: {
enabled: true
},
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
}
}
});
🥈 2. Auth.js (原 NextAuth.js)
Auth.js 已经从 Next.js 中剥离,成为了框架无关的解决方案。
- 优点:生态最庞大,支持几乎所有的 OAuth 提供商和数据库适配器(包括 Drizzle)。社区沉淀深,遇到任何问题都能搜到答案。
- 缺点:它的核心架构较老,配置起来有时显得略微笨重,且对于纯粹的密码登录(Credentials)支持得不够优雅(官方更推崇无密码或 OAuth 登录)。
💡 曾经的王者:Lucia Auth (需注意)
你可能在搜索时会看到很多人推荐 Lucia Auth。它曾是 Drizzle 栈的最佳搭档,以处理 Session 极其优雅著称。但是,Lucia 的作者近期宣布项目进入维护模式(不再积极开发新特性),并亲自推荐社区转向 Better Auth。因此新项目建议直接跳过 Lucia。
总结设计路径
- 鉴权库选择:引入 Better Auth 负责底层复杂的 Token/Cookie 签发、密码 Hash、OAuth 回调和 Drizzle 数据库同步。
- UI 保护:在 TanStack Router 的
beforeLoad中调用 Better Auth 的auth.getSession(),未登录则抛出重定向。 - 数据保护:利用 TanStack Start 的
createMiddleware,在其中校验 Session,并把 User 对象安全地传递给下游的 Drizzle 数据库操作。
浙公网安备 33010602011771号