选择 Next.js 还是 Tanstack?
记录我的技术架构,为什么选择 Tanstack + hono + oRPC
为什么我们想用 TanStack + Hono + oRPC,替代传统 Next.js 技术栈
我们的目标不是“为了新而新”,而是因为我们想做的是:
多端应用 + Cloudflare-first 部署 + 快速 MVP + 长期可维护 API。
传统 Next.js 很强,但它更像一个“大而全 Web 全栈框架”。 而我们现在更想要的是:
前端页面:TanStack Start
后端 HTTP/API:Hono
长期 API 合约:oRPC
部署平台:Cloudflare Workers这套组合的核心价值是:
开发快、访问快、部署轻、多端友好、API 可长期复用。
TanStack Start 官方定位是基于 TanStack Router 的全栈 React 框架,提供 SSR、Streaming、Server Functions、Bundling 等能力;Cloudflare 也已经提供 TanStack Start 的 Workers 部署指南。(TanStack) Hono 自带 RPC 能力,可以通过共享 API specification 让客户端推断输入输出类型;oRPC 则提供 Hono Adapter,更偏向“类型安全 API 合约层”。(Hono)
一、Server Function / Hono API / oRPC 对比
1. 架构决策视角:看痛点和收益
| 类型 | TanStack Server Function | Hono API | oRPC |
|---|---|---|---|
| 一句话理解 | 页面里直接写后端函数 | 轻量高性能 API 框架 | 类型安全 API 合约层 |
| 解决的核心痛点 | 最快把功能做出来 | 后端接口清晰、性能好、Cloudflare 适配好 | 多端调用不失控,API 能长期复用 |
| 最大优势 | 开发快,少写接口样板代码 | 轻、快、标准 HTTP、适合 Webhook/API | 类型安全、接口规范、适合多端和 OpenAPI |
| 对新手友好度 | 高,像写本地函数 | 中,需要理解 API 路由 | 中,需要理解 procedure / contract |
| 对老板的价值 | 快速上线 MVP | 系统边界清晰,未来好扩展 | API 成为资产,方便小程序/App/Agent 复用 |
| 对 Cloudflare 友好度 | 好,跟 TanStack Start 走 | 非常好 | 好,通常挂在 Hono 上 |
| 适合多端吗 | 一般,更偏 Web 内部 | 可以,但需要自己维护类型和文档 | 非常适合 |
| 适合长期产品吗 | 适合页面私有逻辑 | 适合后端服务 | 适合核心业务 API |
| 主要风险 | 逻辑和页面耦合 | 类型、文档、错误格式容易散 | 多一层抽象,小项目可能重 |
| 最适合场景 | MVP、表单提交、页面私有数据 | Webhook、支付、AI 接口、REST API | 小程序、App、Tauri、AI Agent、开放 API |
| 我的建议 | 默认用于页面内部逻辑 | 默认作为后端入口 | 用于长期、多端、核心业务 API |
一句话:
Server Function 负责快,Hono 负责稳,oRPC 负责长期可复用。
2. 工程师视角:看技术边界
| 类型 | TanStack Server Function | Hono API | oRPC |
|---|---|---|---|
| 技术定位 | Full-stack 框架里的 server-only 函数 | HTTP server / router | RPC + API contract |
| 入口形态 | createServerFn() | app.post('/api/posts') | router.posts.create |
| 前端调用方式 | 直接调用函数 | fetch() 或封装 client | client.posts.create() |
| 输入校验 | 可以用 Zod/Valibot | 通常配合 Zod Validator | 通常内置在 procedure/contract 中 |
| 输出类型 | TypeScript 可推断 | 默认不自动推断,除非用 Hono RPC 或手写类型 | 自动推断 |
| 错误处理 | 项目内自定义 | 自己统一 | 更容易统一成 API 层规范 |
| OpenAPI | 不是重点 | 可通过生态实现 | 核心优势之一 |
| 业务语义 | 更像页面动作 | 更像 HTTP endpoint | 更像业务动作 |
| 多端复用 | 弱一些 | 中等 | 强 |
| Webhook 适配 | 一般 | 强 | 可以,但通常仍由 Hono 承载入口 |
| 文件上传/流式响应 | 可以,但不是最自然 | 自然 | 可做业务层,不一定管底层细节 |
| 与 Cloudflare Workers | 好 | 非常好 | 通过 Hono/HTTP adapter 承载 |
| 项目复杂度 | 低 | 中低 | 中高 |
| 最佳使用边界 | 页面私有逻辑 | HTTP/API 边界 | 核心业务 API 合约 |
二、Hono RPC vs oRPC 单独对比
1. 架构决策视角
| 类型 | Hono RPC | oRPC |
|---|---|---|
| 一句话理解 | Hono 自带的类型安全调用方式 | 更正式的类型安全 API 合约系统 |
| 核心价值 | 轻量、简单、少依赖 | 长期、多端、文档化、API 资产化 |
| 适合谁 | 个人项目、小团队、Web 前后端都用 TS | 长期产品、多端应用、ToB、Agent、小程序 |
| 最大优势 | 学习成本低,直接复用 Hono 路由类型 | API 语义更清楚,更适合跨端和开放接口 |
| 最大痛点 | 更偏 TypeScript 内部项目 | 多一层抽象,早期 MVP 会重一点 |
| 是否适合多端 | 一般,主要适合 TS 客户端 | 非常适合 |
| 是否适合 OpenAPI | 不是核心定位 | 核心价值之一 |
| 是否适合快速 MVP | 适合 | 可以,但不是必要 |
| 是否适合长期模板 | 可以 | 更适合 |
| 我的建议 | 小项目先用 Hono RPC | 长期模板/多端项目上 oRPC |
一句话:
Hono RPC 是“轻量内用”;oRPC 是“正式 API 合约”。
2. 工程师视角
| 类型 | Hono RPC | oRPC |
|---|---|---|
| 类型来源 | 从 Hono AppType 推导 | 从 router / procedure / contract 推导 |
| API 组织方式 | HTTP 路由优先 | 业务 procedure 优先 |
| 调用风格 | client.api.posts.$post() | client.posts.create() |
| 路由语义 | 偏 HTTP path/method | 偏业务动作 |
| 前后端耦合 | 前端需要引用 Hono app 类型 | 前端引用 oRPC router/client 类型 |
| TypeScript 项目适配 | 很好 | 很好 |
| 非 TS 客户端 | 一般 | 更好 |
| OpenAPI / API 文档 | 可额外做,但不是核心 | 天然更适合 |
| 中大型项目结构 | 可能受 Hono 路由结构影响 | 更容易按业务模块组织 |
| Monorepo 配置 | 需要处理 AppType 引用 | 也需要处理类型导出,但边界更合约化 |
| 学习成本 | 低 | 中 |
| 最适合 | 内部 Web API | 多端复用 API / 对外 API |
Hono 官方文档里的 RPC 模式是通过 hc<AppType>() 创建客户端,让客户端基于服务端类型推断接口输入输出;oRPC 文档则单独提供 Hono Adapter,说明它通常作为一层 API 合约挂在 Hono 之上。(Hono)
三、Next.js vs TanStack Start 对比
1. 架构决策视角
| 类型 | Next.js | TanStack Start |
|---|---|---|
| 一句话理解 | 成熟的大而全 React 全栈框架 | 更轻、更快、更 Cloudflare-friendly 的 React 全栈框架 |
| 核心优势 | 生态成熟、模板多、Vercel 最顺 | 开发快、性能好、Vite 体系、Cloudflare 适配更自然 |
| 最大痛点 | 臃肿、热重载慢、心智负担重、非 Vercel 部署容易踩坑 | 生态较新,模板和案例少于 Next.js |
| 开发体验 | 项目大后容易重 | 更轻快 |
| 访问性能 | 强,但依赖部署平台和配置 | 适合边缘部署和轻量运行时 |
| Cloudflare 部署 | 能做,但经常需要适配 | Cloudflare 已有官方部署指南 |
| 多端 API 友好度 | Server Actions 偏 Web 内部 | 配合 Hono/oRPC 更适合多端 |
| 内容站 / SEO | 非常成熟 | 可以,但生态不如 Next.js |
| SaaS 模板 | 非常多 | 较少,需要自己搭 |
| 适合快速 MVP | 可以,但偏重 | 非常适合 |
| 适合长期 OPC 模板 | 可以,但维护成本高 | 更符合个人/小团队快速创造 |
| 老板视角结论 | 稳,但重 | 轻、快、适合 Cloudflare-first 和多端战略 |
一句话:
Next.js 适合“成熟 Web 项目”;TanStack Start + Hono + oRPC 更适合“Cloudflare-first 多端产品”。
2. 工程师视角
| 类型 | Next.js | TanStack Start |
|---|---|---|
| 构建体系 | Next 自有体系 / Turbopack / Webpack 生态 | Vite 体系 |
| 路由 | App Router / Pages Router | TanStack Router |
| 类型安全路由 | 一般 | 强 |
| 服务端逻辑 | Route Handler / Server Actions / RSC | Server Functions / Loader / Middleware |
| 数据加载模型 | RSC、fetch cache、revalidate、Server Actions | Loader、Server Functions、Query 组合 |
| 心智负担 | 高 | 中低 |
| 热重载体验 | 大项目容易慢 | 通常更快 |
| Vercel 部署 | 最佳体验 | 可以 |
| Cloudflare 部署 | 可行,但通常需要额外适配 | 路径更自然 |
| Edge/Node 差异 | 容易踩 runtime 差异 | 更偏 Web runtime 思路 |
| API 多端复用 | Route Handler 可做,但 Server Actions 不适合多端 | 配合 Hono/oRPC 更清楚 |
| AI 辅助开发 | 资料多,但框架规则复杂 | 结构清晰,但生态资料较少 |
| 生态成熟度 | 非常成熟 | 新兴 |
| 适合内容站 | 强 | 中 |
| 适合交互应用 | 强,但偏重 | 强且轻 |
| 适合 Cloudflare-first | 一般 | 强 |
四、为什么不是只用 TanStack Server Function?
因为我们确实想做多端。
Server Function 很适合 Web 页面内部逻辑,比如:
保存用户设置
提交表单
读取当前页面 dashboard 数据但它不适合作为长期多端 API 的唯一方案。
如果未来有:
Web
微信小程序
Tauri 桌面端
移动 App
AI Agent
第三方脚本
开放 API那我们需要一个更清晰的 API 层。
所以边界应该是:
| 类型 | 用在哪里 |
|---|---|
| TanStack Server Function | 页面私有逻辑 |
| Hono API | HTTP/API/Webhook 入口 |
| oRPC | 多端复用的核心业务 API |
五、为什么不是只用 Hono API?
只用 Hono API 当然可以。
问题是,随着接口变多,你会开始遇到这些问题:
前端不知道接口要传什么
接口返回值靠人脑记
错误格式不统一
API 文档没人维护
小程序/App/Agent 又要重新对接Hono 解决的是:
怎么接收 HTTP 请求。
oRPC 解决的是:
这个 API 的输入、输出、错误、文档、客户端调用方式如何长期保持一致。
所以我们不是用 oRPC 替代 Hono,而是:
Hono 负责 HTTP 边界
oRPC 负责 API 合约六、为什么不是只用 Hono RPC?
Hono RPC 很好,尤其适合小项目。
但它更适合:
我的 Web 前端 调用 我的 Hono 后端而我们想要的是:
Web
小程序
App
Tauri
AI Agent
外部脚本
第三方系统所以我们更关心:
API 文档
OpenAPI
错误格式
多端调用
长期合约
业务模块化这就是 oRPC 更合适的地方。
七、相同功能代码示例:创建一篇 Post
下面都实现同一个功能:
createPost({
title: string
content: string
})返回:
{
id: string
title: string
content: string
createdAt: string
}为了简化,数据库用一个假函数:
// db/posts.ts
export async function insertPost(input: {
title: string
content: string
}) {
return {
id: crypto.randomUUID(),
title: input.title,
content: input.content,
createdAt: new Date().toISOString(),
}
}方案 A:TanStack Server Function
server:定义 server function
// app/server/posts.ts
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
import { insertPost } from './db/posts'
const CreatePostSchema = z.object({
title: z.string().min(1),
content: z.string().min(1),
})
export const createPost = createServerFn({ method: 'POST' })
.validator((data) => CreatePostSchema.parse(data))
.handler(async ({ data }) => {
const post = await insertPost(data)
return post
})client:页面中直接调用
// app/routes/posts/new.tsx
import { useState } from 'react'
import { createPost } from '@/server/posts'
export default function NewPostPage() {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
async function onSubmit(e: React.FormEvent) {
e.preventDefault()
const post = await createPost({
data: {
title,
content,
},
})
console.log('created post:', post)
}
return (
<form onSubmit={onSubmit}>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="标题"
/>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="内容"
/>
<button type="submit">创建文章</button>
</form>
)
}适合场景
页面内部表单
Dashboard 操作
快速 MVP
不需要给小程序/App/Agent 调用优点是:
最少代码
最快开发
类型体验好缺点是:
不是正式 API 合约
多端复用不自然方案 B:Hono API
server:定义 Hono API
// server/app.ts
import { Hono } from 'hono'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
import { insertPost } from './db/posts'
const app = new Hono()
const CreatePostSchema = z.object({
title: z.string().min(1),
content: z.string().min(1),
})
app.post(
'/api/posts',
zValidator('json', CreatePostSchema),
async (c) => {
const input = c.req.valid('json')
const post = await insertPost(input)
return c.json(post)
},
)
export default appclient:前端 fetch 调用
// app/routes/posts/new.tsx
import { useState } from 'react'
type Post = {
id: string
title: string
content: string
createdAt: string
}
export default function NewPostPage() {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
async function onSubmit(e: React.FormEvent) {
e.preventDefault()
const res = await fetch('/api/posts', {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
title,
content,
}),
})
if (!res.ok) {
throw new Error('创建文章失败')
}
const post = (await res.json()) as Post
console.log('created post:', post)
}
return (
<form onSubmit={onSubmit}>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="标题"
/>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="内容"
/>
<button type="submit">创建文章</button>
</form>
)
}适合场景
Webhook
支付回调
AI 接口
上传接口
后台 API
标准 HTTP 服务优点是:
边界清楚
标准 HTTP
Cloudflare 友好
任何客户端都能调用缺点是:
前端类型需要自己维护
返回值容易靠人脑记
错误格式容易不统一方案 C:Hono + oRPC
下面是偏“结构示意”的写法,具体 API 细节以项目安装的 oRPC 版本为准。核心思想是:业务 API 先定义成 procedure,再挂到 Hono 上暴露。
shared schema
// rpc/schemas/post.ts
import { z } from 'zod'
export const CreatePostSchema = z.object({
title: z.string().min(1),
content: z.string().min(1),
})
export const PostSchema = z.object({
id: z.string(),
title: z.string(),
content: z.string(),
createdAt: z.string(),
})server:定义 oRPC router
// rpc/router.ts
import { os } from '@orpc/server'
import { insertPost } from '../server/db/posts'
import { CreatePostSchema, PostSchema } from './schemas/post'
export const postsRouter = {
create: os
.input(CreatePostSchema)
.output(PostSchema)
.handler(async ({ input }) => {
const post = await insertPost(input)
return post
}),
}
export const appRouter = {
posts: postsRouter,
}
export type AppRouter = typeof appRouterserver:挂到 Hono
// server/app.ts
import { Hono } from 'hono'
import { RPCHandler } from '@orpc/server/fetch'
import { appRouter } from '../rpc/router'
const app = new Hono()
const handler = new RPCHandler(appRouter)
app.use('/rpc/*', async (c, next) => {
const { matched, response } = await handler.handle(c.req.raw, {
prefix: '/rpc',
context: {
// user: c.get('user'),
// env: c.env,
},
})
if (matched) {
return response
}
await next()
})
export default appclient:创建 oRPC client
// rpc/client.ts
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'
import type { RouterClient } from '@orpc/server'
import type { AppRouter } from './router'
const link = new RPCLink({
url: '/rpc',
})
export const client: RouterClient<AppRouter> = createORPCClient(link)client:页面调用
// app/routes/posts/new.tsx
import { useState } from 'react'
import { client } from '@/rpc/client'
export default function NewPostPage() {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
async function onSubmit(e: React.FormEvent) {
e.preventDefault()
const post = await client.posts.create({
title,
content,
})
console.log('created post:', post)
}
return (
<form onSubmit={onSubmit}>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="标题"
/>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="内容"
/>
<button type="submit">创建文章</button>
</form>
)
}适合场景
Web + 小程序 + App
Tauri 桌面端
AI Agent 调用
开放 API
长期业务系统
需要 API 文档
需要统一错误格式优点是:
前端调用像本地函数
输入输出类型自动对齐
API 语义清晰
未来更适合多端缺点是:
多一层抽象
早期小项目可能显得复杂oRPC 的客户端文档也展示了 createORPCClient、RPCLink、RouterClient 这类模式,核心就是把服务端 router 暴露成类型安全客户端。(GitHub)
八、三种代码写法对比总结
| 类型 | TanStack Server Function | Hono API | Hono + oRPC |
|---|---|---|---|
| 前端调用体验 | createPost({ data }) | fetch('/api/posts') | client.posts.create() |
| 后端组织方式 | 函数 | HTTP 路由 | 业务 procedure |
| 样板代码 | 少 | 中 | 中 |
| 类型安全 | 强 | 弱到中 | 强 |
| 多端复用 | 一般 | 中 | 强 |
| API 文档 | 弱 | 中 | 强 |
| 适合新手 | 最简单 | 中 | 中 |
| 适合长期架构 | 中 | 强 | 最强 |
| 最适合 | 快速页面逻辑 | HTTP/API/Webhook | 多端核心业务 API |
九、我们最终的推荐架构
推荐分层
apps/web
TanStack Start
TanStack Router
React
Tailwind / shadcn
server
Hono
middleware
auth
webhook
api boundary
rpc
oRPC router
schemas
procedures
client
db
Drizzle
D1 / Postgres请求流
Web 页面
↓
TanStack Start
↓
页面私有逻辑:Server Function
↓
多端业务逻辑:oRPC Client
↓
Hono /rpc/*
↓
oRPC Router
↓
Drizzle / D1 / Postgres边界规则
| 需求 | 放哪里 |
|---|---|
| 页面内部提交表单 | TanStack Server Function |
| Dashboard 私有操作 | TanStack Server Function |
| 支付回调 | Hono API |
| 微信小程序登录 | Hono API / oRPC |
| AI 模型代理 | Hono API |
| 核心业务 API | oRPC |
| 未来给 App / 小程序 / Agent 复用 | oRPC |
| 对外开放接口 | oRPC + OpenAPI |
| 静态内容 / 文档 / SEO | Next.js / Fumadocs / Astro |
十、最终结论
我们不是简单地说:
TanStack 比 Next.js 好
Hono 比 Next.js 好
oRPC 比 Hono 好而是说:
对于多端应用、Cloudflare-first 部署、快速 MVP、长期 API 复用这个目标,TanStack + Hono + oRPC 的组合比传统 Next.js 更适合。
最终选择可以这样定:
| 项目阶段 | 技术选择 |
|---|---|
| 快速 MVP | TanStack Start + Server Functions |
| 有后端接口 | TanStack Start + Hono |
| 多端产品 | TanStack Start + Hono + oRPC |
| 内容站/文档站 | Next.js / Fumadocs / Astro |
| 标准 SaaS / Vercel-first | Next.js |
| Cloudflare-first 长期模板 | TanStack Start + Hono + oRPC + Drizzle + D1/Postgres |
一句话总结:
TanStack Start 让我们更快做 Web,Hono 让我们更稳地跑 API,oRPC 让我们的 API 能服务多端。 这就是我们选择 TanStack + Hono + oRPC,而不是继续默认 Next.js 的原因。