App API 总览
原生客户端与 Web 应用共用的版本化 /api/app/v1 后端。
模板内置一套版本化 App API(/api/app/v1)——功能完备的 HTTP 后端,供
Swift、Kotlin、Expo、Electron 与 Tauri 直接调用。Next.js、Nuxt 与 TanStack Start
实现相同公开契约。Web 页面与客户端共享同一个 Better Auth 用户、同一个数据库、同一条订阅:在
iOS 上用 Web 注册的账户登录、读取同一份数据,并且任一渠道购买都会解锁同一份
权益。
提供的能力
/api/app/v1下的 15 个业务端点:Profile、Todos、Device Token、APNs 测试推送、订阅快照 + 管理入口、presigned 上传、账户删除。- 一个 Service 层(
src/server/app/),路由处理器与 Web 页面共同调用—— 业务逻辑只写一次。 - 所有端点均使用 Bearer Session 鉴权;资源归属始终来自服务端 Session, 绝不接受客户端提交的用户 ID。
- 机器可读的 OpenAPI 3.1 契约:
docs/openapi.yaml。
认证本身没有在 /api/app/v1 里重新发明。原生客户端携带 Bearer token
调用同一套 /api/auth/* Better Auth 端点——见
原生认证。
重要文件
| 文件 | 作用 |
|---|---|
src/app/api/app/v1/ | 路由处理器(只负责 HTTP 边界) |
src/server/app/shared/ | Session 守卫、响应 Envelope、Request ID、日志、限流、幂等 |
src/server/app/{profile,todos,devices,push,subscriptions,uploads,account}/ | 业务 Service |
src/lib/db/schema/app.ts | todos 与 device_tokens 表 |
docs/openapi.yaml | OpenAPI 3.1 契约——单一事实来源 |
架构
每个路由都是一层薄封装:authedRoute 构建请求上下文、强制校验 Session,并把
抛出的 AppError 映射为统一错误 Envelope。处理器把工作委托给 Service,后者
接收服务端确认的用户 ID:
export const GET = authedRoute(async (ctx) => {
const items = await todosService.list(ctx.userId, filter);
return ok({ items }, ctx);
});Service 不导入 NextRequest/NextResponse,因此同一批函数同时支撑 Web
仪表盘和 App API。
API 约定
| 事项 | 规则 |
|---|---|
| 鉴权 | 每个请求携带 Authorization: Bearer <sessionToken> |
| JSON 命名 | camelCase |
| 日期 | ISO 8601 UTC(2026-07-13T04:42:00.000Z) |
| 成功 Envelope | { "success": true, "data": … } |
| 错误 Envelope | { "success": false, "error": { "code", "message", "requestId" } } |
| 错误码 | 稳定的 SCREAMING_SNAKE_CASE 枚举——按 code 分支,不要解析 message |
| Request ID | 可传 X-Request-Id,否则服务端生成;在响应 header 和错误体中回显 |
| 幂等 | 非幂等写操作接受 Idempotency-Key header |
| 版本策略 | v1 只做向后兼容变更;破坏性变更以 v2 并行发布 |
错误行为
| HTTP | 错误码(示例) | 触发条件 |
|---|---|---|
| 400 | INVALID_REQUEST | Zod 校验失败、请求体格式错误 |
| 401 | UNAUTHENTICATED | Session 缺失、失效或已撤销 |
| 404 | *_NOT_FOUND | 资源不存在或属于其他用户 |
| 409 | SUBSCRIPTION_ALREADY_ACTIVE | 有效订阅期间重复购买 |
| 422 | UNPROCESSABLE | 语义校验失败(如不支持的上传类型) |
| 429 | RATE_LIMITED | 触发限流 |
| 500 | INTERNAL | 未预期错误,不泄露内部细节 |
越权访问返回 404 而不是 403。 每个资源查询同时带资源 ID 和 Session 的 用户 ID 条件,因此 API 永远不会暴露其他用户的资源是否存在。
端点
| 方法 | 路径 | 用途 |
|---|---|---|
| GET / PATCH | /api/app/v1/profile | 读取 / 更新开放的资料字段 |
| GET / POST | /api/app/v1/todos | 列出 / 创建 todos |
| PATCH / DELETE | /api/app/v1/todos/:id | 更新 / 删除单个 todo |
| POST | /api/app/v1/todos/clear-completed | 删除已完成的 todos |
| PUT | /api/app/v1/todos/reorder | 持久化完整排序 |
| PUT / DELETE | /api/app/v1/device-tokens[/:token] | 注册 / 删除 APNs token |
| POST | /api/app/v1/notifications/test-push | 向当前用户的设备发送测试推送 |
| GET | /api/app/v1/subscription | 当前订阅快照 + isEntitled |
| POST | /api/app/v1/subscription/portal | 支付平台托管的管理 URL(或 null) |
| POST | /api/app/v1/uploads | 申请 presigned 直传 URL |
| POST | /api/app/v1/uploads/complete | 确认上传并获取可用 URL |
| DELETE | /api/app/v1/account | 删除账户并级联清理所属数据 |
另有 PUT /api/app/v1/uploads/local,是本地存储 Provider 的仅开发环境
上传目标;生产环境禁用,不属于契约。
OpenAPI 契约
docs/openapi.yaml 描述了每个端点、Schema、枚举和错误响应,是客户端实现的
单一事实来源——可直接用于任何 OpenAPI 查看器或生成器,并在契约测试中校验
手写的客户端模型。
