FlareStarter 文档
平台与运维

REST API 架构

/api/v1 业务 API —— Hono 端点、类型化客户端、problem+json 错误契约与 SSR 进程内分发。

浏览器可见的业务操作全部走版本化的 REST 契约 /api/v1/*。TanStack Start 负责 路由、SSR、hydration 与 SEO;Hono(src/server/api/)负责应用 API。两端在 同一个 Cloudflare Worker 里按路径分发(/api/v1/* → Hono,其余 → TanStack)。

边界:什么进 /api/v1,什么不进

  • :所有业务操作 —— 公共配置、会话/账户、waitlist、projects、leads、 feedback、billing、sponsorship、admin、docs/changelog 元数据 (src/server/api/routes/*,每个 feature 一个路由模块)。
  • 不进:协议/资产/文档表面 —— Better Auth(/api/auth/*)、Stripe webhook、 头像读取(/api/avatars/*)、docs markdown 与搜索(/docs-md/*)、admin CSV 导出、sitemap/robots/llms。它们各有协议级的安全模型(签名、session、权限), 不套用业务 API 的中间件链。

加一个端点的固定流程

以「文章详情」为例,五步:

// 1. schema —— src/server/api/schemas/posts.ts(纯 zod,禁止 import hono/env:
//    同一份 schema 驱动运行时校验、OpenAPI 文档与客户端返回类型)
import { z } from 'zod'
export const PostResponseSchema = z.object({
  id: z.string(),
  title: z.string(),
})
export type PostResponse = z.infer<typeof PostResponseSchema>
// 2. 路由 —— src/server/api/routes/posts.ts(handler 内惰性 import 服务层;
//    业务规则留在 feature 的 *.server.ts,不下沉到 handler)
import { Hono } from 'hono'
import { describeRoute, resolver } from 'hono-openapi'
export const postRoutes = new Hono<HonoEnv>()
postRoutes.get(
  '/posts/:id',
  requireUser(),                       // 401 problem,不重定向
  validate('param', PostIdParamSchema), // 运行时校验 + 自动登记 OpenAPI 参数
  describeRoute({
    operationId: 'getPost',
    tags: ['Posts'],
    security: [{ cookieAuth: [] }],
    responses: { 200: { content: { 'application/json': { schema: resolver(PostResponseSchema) } } } },
  }),
  async (c) => {
    const { getPost } = await import('@/features/posts/posts.server')
    return c.json(await getPost(createDb(c.env.DB), c.req.valid('param').id))
  },
)
// 3. 注册 —— src/server/api/app.ts: app.route('/api/v1', postRoutes)
// 4. 客户端 op —— src/lib/api/operations.ts(type-only import 保持浏览器包干净)
export function getPost(postId: string, opts: OperationOptions = {}): Promise<PostResponse> {
  return apiRequest<PostResponse>(`/api/v1/posts/${encodeURIComponent(postId)}`, opts, ...)
}
//    并在 src/lib/api/index.ts 导出
# 5. 再生成契约(仓库根 openapi.json 是金样,过期时 openapi.node.test.ts 失败)
pnpm openapi:update

配套测试:<模块名>.workers.test.ts(真实 D1 的端点契约测试,与路由模块同目录)。

中间件链与鉴权

src/server/api/app.ts 的固定顺序:

requestId → logger → errorHandler → csrf(不安全方法) → sessionUser → 路由
  • CSRF:不安全方法校验精确配置的 origin + Sec-Fetch-Site: same-origin (本地开发放行 localhost);普通 JSON mutation 还要求 Content-Type: application/json(否则 415)。无 token、无 permissive CORS。
  • 会话:sessionUser 从 Better Auth 的 HttpOnly cookie 派生身份;路由内用 requireUser() / admin 门禁 —— REST 只回状态码,绝不重定向:401 未认证、 404 隐藏存在(非本人/非管理员同形)、403/409/422/429 按语义。
  • 前端解释状态:loader 用 requireUserClient(401 → locale 正确的登录页 redirect)、requireAdminClient(401 → 登录页,404 → notFound 边界), 组件 mutation 按错误码映射 i18n 文案 —— 都来自 @/lib/api

错误契约:application/problem+json

所有错误共用统一信封(RFC 9457):{ type, title, status, code, requestId, detail?, errors? }code 是稳定枚举(src/server/api/problems.tsERROR_CODES,只增不改),requestIdx-request-id 响应头及服务端日志关联; 字段级校验失败带 errors[](field/code)。客户端(src/lib/api)把非 2xx 统一 解析成 ApiError,isApiError / err.status / err.code 驱动 UI 分支。

SSR:进程内分发,无公网回环

src/lib/api 环境自适应:

  • 浏览器:same-origin fetch('/api/v1/...')(带 cookie、abort signal)。
  • SSR(loader 在 Worker 里跑):构造标准 Request 进程内递给同一个 Hono 实例(src/server/api/ssr.ts)—— 不向部署域名发回环请求;cookie、 locale、请求头按白名单转发,CSRF/会话判定输入与真实请求一致。

关键数据在 loader 里渲染前取回,SEO 元数据(canonical/og/正文)全部留在 服务端渲染的 HTML 里;公开只读端点带 Cache-Control(config/changelog 60s、 docs 300s —— 内容只在部署时变化)。

On this page