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-ormzod 的桥梁。
  • 在 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 页面上直接调试真实的数据加载效果,而不需要手动造数据。

总结:它们是如何协同完成一次请求的?

想象一个用户在你的应用中提交了一个“添加任务”的表单:

  1. 用户操作:点击提交按钮。
  2. 网络层(Nitro):接收到这个 POST 请求,将其路由到对应的 Server Function。
  3. 校验层(zod + drizzle-zod):检查用户输入的内容是否为空、是否符合我们在数据库定义的格式。
  4. 持久层(drizzle-orm + better-sqlite3):校验通过后,将数据转换为 SQL INSERT 语句,同步写入本地的 SQLite 数据库。
  5. 渲染层(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. 开发建议总结

  1. 版本控制: 虽然现在是 /api/user,但为了长远考虑,建议目录改为 app/routes/api/v1/users.ts。这样当你的 API 发生重大破坏性变更时,可以发布 v2 而不影响旧的 Android 客户端。
  2. 状态码要准确:
    • 201: 已创建 (POST 成功)
    • 204: 无内容 (DELETE 成功)
    • 409: 冲突 (唯一键冲突)
    • 422: 语义错误 (Zod 校验失败)
  3. 安全性: 在生产环境下,确保 API 路由开启了 CORS 限制(通过 app.config.ts 中的 nitro 配置),只允许特定的域名或移动端 User-Agent。

通过这种 Controller -> Service -> DB 的分层结构,即便以后你要从 SQLite 迁移到 PostgreSQL,或者业务逻辑变得极其复杂,你的 API 结构依然能保持清晰。

这是一个非常敏锐的观察。你之所以找不到 index.htmlmain.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()]

这两个插件在起作用:

  1. tanstackStart():它会告诉 Vite:“嘿,别找 main.ts 了。我会根据用户的 src/routes 目录自动生成一个客户端入口和服务器入口。”
  2. nitro():它负责启动底层的 Web 服务器,处理 API 路由(比如你写的 /api/user)和页面渲染请求。

虽然在磁盘上你看不到,但在开发模式下,Vite 内存中存在类似 virtual:tanstack-start-client-entry 的文件,它负责执行 ReactDOM.hydrateRoot()


3. 项目的启动流程 (The Boot Sequence)

当你运行 pnpm dev 时,发生了以下事情:

  1. 扫描路由tanstack-router 扫描 src/routes 目录,并自动生成 src/routeTree.gen.ts。这个文件就是你项目的“地图”。
  2. 启动 Nitro 服务器:Nitro 启动并接管 3000 端口。
  3. 路由匹配:当你访问 / 时,服务器查看 routeTree.gen.ts,发现应该渲染 src/routes/index.tsx
  4. 渲染层级
  • 服务器先渲染 __root.tsx(外壳)。
  • 然后在 __root.tsx<Outlet/> 位置渲染 index.tsx(内容)。
  1. 注水 (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。

总结设计路径

  1. 鉴权库选择:引入 Better Auth 负责底层复杂的 Token/Cookie 签发、密码 Hash、OAuth 回调和 Drizzle 数据库同步。
  2. UI 保护:在 TanStack Router 的 beforeLoad 中调用 Better Auth 的 auth.getSession(),未登录则抛出重定向。
  3. 数据保护:利用 TanStack Start 的 createMiddleware,在其中校验 Session,并把 User 对象安全地传递给下游的 Drizzle 数据库操作。
posted @ 2026-08-02 11:13  tommao9925  阅读(13)  评论(0)    收藏  举报