最近在整理一套可以长期复用的 TypeScript 项目模板。
我的项目大多是 AI 工具、管理后台、工作流、编辑器和桌面端配套应用。技术栈以 React、TypeScript 和 Vite 为主,后端会使用 Node.js、PostgreSQL 和 Drizzle。
一开始考虑的是直接选择一个 React 全栈框架,把路由、SSR、数据请求、API 和部署统一起来。重点看了 Next.js、React Router Framework Mode 和 TanStack Start,也顺带关注了 Remix 3。
研究了一段时间后,我发现框架能力并不是越完整越合适。对于偏重交互的应用,SSR 和同构运行模型会引入不少额外概念,而这些概念未必能直接转化为产品收益。
最后确定的方向是:
公开站点使用 SSR 或 SSG
登录后的产品使用 SPA
业务能力通过统一 HTTP API 提供
这篇文章记录一下具体的判断过程。
TanStack Start 的吸引力
TanStack Start 很符合我对现代 TypeScript 框架的期待。
它建立在 TanStack Router 和 Vite 之上,提供类型安全路由、Search Params、Loader、Server Function、Middleware 和 SSR。再配合 TanStack Query、Form 和 Table,可以形成一套完整的前端应用开发体系。
一个简单的服务端查询可以写成:
路由中直接调用:
组件通过路由读取结果:
这种写法不需要手动维护 HTTP Client 和返回类型。Server Function 在客户端表现得像一个普通的异步函数,输入和输出类型可以直接推导。
对于简单页面,这套模式很顺畅。
问题主要出现在页面交互开始变复杂之后。
例如,列表需要手动刷新,可以调用:
如果列表使用 TanStack Query,则会写成:
新增一条记录后,可以重新执行路由 Loader:
也可以让 Query Cache 失效:
这时项目里可能同时存在两套数据生命周期:
Route Loader 管理路由数据
TanStack Query 管理客户端缓存
如果继续加入 Query Options、Mutation、TanStack Form、TanStack Table 和 Zod,一个普通 Feature 很容易拆出很多文件:
project.server.ts
project.functions.ts
project.queries.ts
project.mutations.ts
project.schema.ts
project.form.ts
project.columns.tsx
这些模块单独看都有合理用途,但组合起来后,简单业务的框架代码占比会比较高。
TanStack 生态本身没有要求项目必须这样写。问题在于它提供的能力很多,开发者很容易在项目初期就把完整方案铺开。
对于复杂 Dashboard、实时数据、无限列表和乐观更新,这些抽象能够发挥作用。对于普通 CRUD,它们可能显得偏重。
Next.js 的复杂度在运行边界
Next.js 的代码通常更短。
Server Component 可以直接查询数据库:
修改操作可以写成 Server Action:
页面直接提交:
这种模式对内容站、电商和以服务端渲染为主的页面很合适。页面、数据读取和修改逻辑可以放在相近的位置,项目初期的代码量也不大。
随着客户端交互增加,代码会逐渐涉及:
Server Component
Client Component
Server Action
Route Handler
Suspense
缓存失效
序列化边界
Next.js 的额外成本通常不表现为大量包装函数,而是运行模型和框架规则。
对于编辑器、工作流和管理后台,页面里往往有较多本地状态、轮询、实时数据、弹窗和多区域交互。此时 Server Component 与 Client Component 的边界需要持续维护。
Next.js 依然是成熟的生产方案,只是它更偏向以服务端组件和页面渲染为中心组织应用,不完全符合我目前的产品形态。
React Router 的数据模型比较统一
React Router Framework Mode 使用 Loader 和 Action 组织路由数据。
它的模型比较接近传统 Web 开发:
读取数据使用 loader
修改数据使用 action
页面提交使用 Form 或 fetcher
Action 完成后,React Router 会重新验证相关 Loader,因此普通表单场景不需要额外维护 Query Cache。
这种方式适合表单和路由驱动的应用。Request、Response、Cookie、Session 和上传都使用标准 Web API,调试路径也比较清楚。
页面内操作较多时,Action 可能需要根据 intent 分发不同业务:
也可以拆成独立 Resource Route。
React Router 的代码量通常介于 Next.js 和完整 TanStack 方案之间。它适合传统 Web 数据流,但对于复杂客户端缓存和多个组件共享远程状态,项目最终仍可能引入 TanStack Query。
SPA 的数据流更适合产品后台
把几个全栈框架放在一起比较之后,我重新审视了普通 SPA。
一个 SPA 项目的远程数据流很直接:
React Component
↓
TanStack Query
↓
HTTP API
↓
业务逻辑
↓
数据库
查询:
手动刷新:
新增后刷新列表:
这套模型只有一份远程数据状态。
查询使用 useQuery
修改使用 useMutation
缓存刷新使用 invalidateQueries
页面参数使用 Router Search Params
本地状态使用 useState 或 Jotai
对于登录后的管理系统,这样的模型已经足够。
管理后台、AI 工具和工作流应用通常不会依赖搜索引擎索引内部页面。用户登录后会停留较长时间,主要操作发生在浏览器端。
SSR 在这些页面中的收益有限,但会增加 Hydration、服务端与客户端模块边界、Loader 生命周期和缓存同步等问题。
因此,我决定不让整个产品都进入 SSR 模型。
SSR 与 SPA 按路由区域拆分
最终的产品路由可以分成三个区域:
公开区域使用 SSR 或 SSG,负责:
- SEO;
- 首屏内容;
- Open Graph;
- 博客和文档;
- 产品介绍;
- 公开工具页面。
/app 区域使用 SPA,负责:
- Dashboard;
- 工作流;
- 编辑器;
- AI 生成;
- 实时数据;
- 复杂表格;
- 多面板交互。
API 负责:
- 认证;
- 权限;
- 业务逻辑;
- 数据库访问;
- 文件上传;
- Webhook;
- Desktop 和其他客户端。
这样每个区域只维护一种主要数据模型。
最终的 Monorepo 结构
目前比较适合我的结构是:
各应用的职责如下:
共享包保持克制。
packages/ui 存放基础组件、样式 Token 和动画预设。
packages/contracts 存放真正需要跨客户端共享的 Zod Schema、错误结构和 API 类型。
packages/db 存放 Drizzle Schema、Migration 和数据库 Client。
业务代码默认留在具体 App 中,不会在项目开始阶段就提取到 packages/core。
API 使用 Hono
API 层选择 Hono。
它的路由写法比较轻:
客户端可以使用 Hono RPC:
在 TanStack Query 中调用:
完整调用链如下:
Component
↓
TanStack Query
↓
Hono RPC Client
↓
Hono Route
↓
Feature Function
↓
Drizzle
这套 API 可以同时提供给 Web、Desktop、浏览器扩展、Agent 和 Webhook。
相比框架内部专用的 Server Function,HTTP API 的复用范围更大。
后端按 Feature 组织
前后端拆分不意味着后端需要复制 Java 的分层方式。
不会采用这种全局目录:
controllers/
services/
repositories/
mappers/
dto/
API 仍然按 Feature 组织:
简单 Feature 只保留必要文件:
project.routes.ts
project.schema.ts
数据库实例可以直接导入:
只有存在多个实现、复杂测试或运行时替换需求时,才使用参数注入或构造器注入。
不会为了保持架构形式,为每张表创建 Repository、Service 和 Mapper。
工具链保持激进,运行层保持稳定
项目工具链会使用相对新的方案:
Vite
Rolldown
Oxc
Oxlint
Oxfmt
Vite+
Vitest
Playwright
这些工具不会直接处理线上业务数据。即使某个版本出现兼容问题,也可以回退到对应的底层命令。
应用层使用:
React
TanStack Router
TanStack Query
TanStack Form
TanStack Table
Jotai
Tailwind CSS
shadcn/ui
Base UI
Motion
Lucide
其中 TanStack Form、Table 和 Jotai 都按需使用,不作为每个页面的固定依赖。
数据与安全层相对保守:
Node.js LTS
PostgreSQL
Drizzle ORM
Better Auth 稳定版本
Zod
正式数据库 Migration
Cookie Session
工具升级失败会影响开发效率,认证和数据库升级失败则可能影响用户和数据。两部分不应该使用相同的升级策略。
部署方式
源码上拆成多个 App,不代表生产环境必须维护大量服务。
可以使用同一个域名:
example.com/
→ Site
example.com/app/*
→ SPA
example.com/api/*
→ API
网关按路径分流:
/ → Site
/app/* → Web
/api/* → API
认证保持同源 Cookie。SPA 调用 /api 时不需要处理跨域 Token,Desktop 等外部客户端再使用单独的认证方式。
个人项目早期也可以让 Hono 同时提供 API 和 SPA 静态文件,保持一个 Docker 镜像和一个 Node 进程。
结论
这次技术选型没有确定一个统一管理所有能力的全栈框架,而是把不同类型的页面放回它们更适合的运行模型中。
公开内容使用 SSR 或 SSG
登录后的产品使用 SPA
通用业务能力使用 HTTP API
最终技术栈大致是:
这套结构保留了现代 TypeScript 工具链,也让业务后台继续使用熟悉的 SPA 数据流。
框架能力可以按项目需求增加。对于大多数偏工具型的产品,保持清楚的客户端、API 和数据边界,比在每个页面中同时组合 SSR、Loader、Server Function 和 Query 更容易维护。