Skip to content

API 规范 ​

后端 API 由 packages/server/src/app.ts 统一装配,业务路由统一挂载在 /api 前缀下。常规业务接口使用 Hono + @hono/zod-openapi,以 Zod schema 同时驱动运行时校验与 OpenAPI 文档。

统一响应格式 ​

成功响应统一由 okBody(data, message?) 构造:

json
{
  "code": 0,
  "message": "success",
  "data": {}
}

失败响应统一由 errBody(message, code?) 构造,code 与 HTTP 状态码保持同语义,data 为 null:

json
{
  "code": 404,
  "message": "资源不存在",
  "data": null
}

路由 handler 中使用 return c.json(okBody(...), 200) 或 return c.json(errBody(...), status),不要内联 { code, message, data } 字面量。文件下载类响应通过 okExcel() / okCsv() / okFile() 声明 OpenAPI,handler 中使用 excelBody() / excelStreamBody() / csvStreamBody() / fileBody() 或直接返回 Response。

分页返回格式 ​

列表接口返回 PaginatedResponse<T>,并放在统一响应的 data 字段内:

json
{
  "code": 0,
  "message": "success",
  "data": {
    "list": [],
    "total": 100,
    "page": 1,
    "pageSize": 10
  }
}

分页查询参数在契约中用 paginationQuery.extend({ ... }) 声明,分页响应用 paginated(xxxSchema)。默认 page=1、pageSize=10,pageSize 最大 200。Service 层 SQL-builder 查询使用 withPagination(query.$dynamic(), page, pageSize);RQB 查询使用 pageOffset(page, pageSize)。

查询参数的 TypeScript 类型只从契约操作派生,不在 service 手写 interface、也不在契约文件逐个导出 z.infer 别名:

  • Server:QueryOutputOf<typeof xxxContract.list>(@zenith/shared/core)——解析后输出,page / pageSize 为必填 number, queryEnum / entityStatusQuery 的空串已归一为 undefined,与路由 c.req.valid('query') 同型,service 里因此不再写 page = 1 之类默认值。
  • Web:QueryOf<typeof xxxContract.list>——客户端视角,带默认值的字段可省略。
  • 筛选条件需与导出中心等不带分页的调用方共用时,定义一个 Omit<QueryOutputOf<…>, 'page' | 'pageSize'> 的筛选类型。

查询串里的启用 / 禁用状态筛选用 entityStatusQuery,其它枚举用 queryEnum(XXX_VALUES, { dict | options })(第二参数声明标签来源,前端筛选下拉据此取标签),布尔用 queryBool():三者都把筛选控件清空后发出的空串视为「全部」;entityStatusSchema 只用于请求体与实体字段。

日期时间格式 ​

所有对外 API 响应和业务日期时间入参统一使用 YYYY-MM-DD HH:mm:ss,例如:2026-03-22 20:09:37。

  • 数据映射、导出和文件时间戳使用 packages/server/src/lib/datetime.ts 中的 formatDateTime() / formatNullableDateTime() / formatDate() / formatFileTimestamp()。
  • 单点时间入参使用 parseDateTimeInput()。
  • 范围端点使用 parseDateRangeStart() / parseDateRangeEnd(),或直接使用 dateRangeConditions()。
  • 路由查询 schema 中的范围端点用 ...dateRangeQuery('说明')(标准 startTime / endTime)或 dateRangeBound('说明', 'start' | 'end')(自定义键名),接受 YYYY-MM-DD 与 YYYY-MM-DD HH:mm:ss。
  • 业务接口契约不要使用 ISO datetime,数据映射不要直接 toISOString()。
  • 数据库存储、会话时区与 SQL 写法见数据库与迁移 → 时间与时区。

认证方式 ​

管理端使用 Access Token + Refresh Token:

Token前端存储 Key说明
Access Tokenzenith_token短期凭证,通过请求头传递
Refresh Tokenzenith_refresh_token长期凭证,用于 /api/auth/refresh 换发 Access Token

需要认证的请求携带:

http
Authorization: Bearer <accessToken>

管理端 authMiddleware 同时支持以 zat_ 开头的 API Token。管理员 token 与会员 token 严格隔离:authMiddleware 拒绝 type: 'member' 的 token,会员接口由 memberAuthMiddleware 校验 type: 'member'。

认证中间件会在 Hono 上下文中注入 user。路由守卫可通过 c.get('user') 读取;Service 层统一使用 currentUser() / currentUserOrNull(),避免在 route handler 与 service 之间透传 Context。

ts
import { currentUser } from '../lib/context';

const user = currentUser();

参数校验与路由声明 ​

每个端点由 @zenith/shared/{业务域}/contracts/ 中的契约操作定义:方法、路径、params / query / headers / body schema 与响应 schema(headers 只声明业务请求头,如幂等键 x-idempotency-key;认证头由 security 表达)。 标准操作(list / detail / create / update / remove / removeBatch)由 mountCrud(router, contract, service) (routes/_crud.ts)按契约派生路由:权限 / 审计取契约操作的 access / audit,更新 / 删除前的实体快照统一,DELETE /batch 先于 /{id}; 非标准操作用 defineContractRoute(op, { handler })(lib/contract-route.ts) 把契约变成 Hono 路由。两者的入参都按契约 schema 校验,由 validationHook 统一转为标准错误响应。

json
{
  "code": 400,
  "message": "<Zod 校验错误信息>",
  "data": null
}

契约(packages/shared/src/platform/contracts/xxxs.ts):

ts
import * as z from 'zod';
import { auditFieldsSchema, defineContract, idParam, op, paginated, paginationQuery } from '../../core';
import { createXxxSchema, updateXxxSchema } from '../validation';

export const xxxSchema = z.object({
  id: z.int(),
  name: z.string(),
  ...auditFieldsSchema,
  createdAt: z.string(),
  updatedAt: z.string(),
}).meta({ id: 'Xxx' });
export type Xxx = z.infer<typeof xxxSchema>;

// 每个后台登录令牌操作必须声明 access(权限码 / 'authenticated' / platformOnly),写操作声明 audit;
// 服务端门禁、前端按钮、Demo Mock、OpenAPI 与权限矩阵都从这里读取
export const xxxContract = defineContract('/api/xxxs', {
  list: op.get('/', { access: { permission: 'system:xxx:list' }, query: paginationQuery.extend({ keyword: z.string().optional() }), response: paginated(xxxSchema), summary: 'XXX 列表' }),
  detail: op.get('/{id}', { access: { permission: 'system:xxx:list' }, params: idParam, response: xxxSchema, summary: 'XXX 详情' }),
  create: op.post('/', { access: { permission: 'system:xxx:create' }, audit: '创建 XXX', body: createXxxSchema, response: xxxSchema, summary: '创建 XXX' }),
  update: op.put('/{id}', { access: { permission: 'system:xxx:update' }, audit: '更新 XXX', params: idParam, body: updateXxxSchema, response: xxxSchema, summary: '更新 XXX' }),
  remove: op.delete('/{id}', { access: { permission: 'system:xxx:delete' }, audit: '删除 XXX', params: idParam, summary: '删除 XXX' }),
}, { tags: ['Xxx'], auditModule: 'XXX 管理' });

路由(packages/server/src/routes/platform/xxxs.ts):

ts
import { OpenAPIHono } from '@hono/zod-openapi';
import { xxxContract } from '@zenith/shared/platform';
import { defineContractRoute } from '../../lib/contract-route';
import { okBody, validationHook } from '../../lib/openapi-schemas';

const xxxRouter = new OpenAPIHono({ defaultHook: validationHook });

// 认证 / 权限 / 审计按契约 access / audit 自动装配,路由只写 handler;认证前的限流放 preAuth,认证后追加的中间件放 middleware
const createXxxRoute = defineContractRoute(xxxContract.create, {
  handler: async (c) => c.json(okBody(await createXxx(c.req.valid('json')), '创建成功'), 200),
});

xxxRouter.openapiRoutes([createXxxRoute] as const);
export default xxxRouter;

要点:

  • 每个 OpenAPIHono 实例传入 { defaultHook: validationHook }。
  • 每个路由用命名常量声明,并通过 router.openapiRoutes([... ] as const) 统一注册;挂载路径取 xxxContract.basePath。
  • 认证与权限不在路由文件出现:后台登录令牌操作的 access / audit / feature 声明在契约上,defineContractRoute 据此装配 preAuth → authMiddleware → platformAdminOnly → guard(权限 / 审计 / 功能门控),app.contract.test.ts 逐端点对账契约与运行时门禁。 文档中的 security 同样由契约推导:默认 BearerAuth,公开接口标 public: true(security: []),会员前台契约组整组标 { security: 'member-bearer' }(MemberBearerAuth),IoT 设备签名与开放平台网关接口标 security: 'device-signature' | 'open-gateway'(对应 IotDeviceSignature / OpenGatewayToken + OpenGatewaySignature 安全方案,由 lib/contract-route.ts 的 CONTRACT_SECURITY_SCHEMES 注册;这些非后台令牌操作的鉴权 / 验签中间件写在路由 middleware:)。 commonErrorResponses 与 200 响应信封由适配层统一施加。
  • handler 内 c.req.valid('param' | 'query' | 'json') 与 c.json(okBody(...), 200) 均按契约类型检查: service 返回值与契约实体不一致时编译失败。
  • 路径参数用 idParam;自定义路径参数写 z.object({ code: z.string().meta({ description, example }) }); OpenAPI 元数据一律用 zod 原生 .meta()(组件名 .meta({ id }))。
  • 部分更新(PUT / PATCH)的请求体 schema 一律用 partialForUpdate(createXxxSchema)(@zenith/shared/core)派生, 它会剥离全部 .default() 再置为可选:省略的字段表示「保持不变」。禁止直接调用 .partial()(ESLint 封禁), 契约测试会拒绝任何请求体属性携带 default 的 PUT / PATCH 操作;全量替换 / upsert 端点需在 src/app.contract.test.ts 的整体替换例外清单登记理由。
  • 全量集合赋值端点(PUT /{id}/roles 等)的集合字段必填,不得 .default([]):字段缺失应校验失败而非静默清空。
  • 上传接口的请求体用 multipart(z.object({ file: fileField() })) 声明,handler 内 c.req.parseBody() 读取; 文件类响应用 kind: 'excel' | 'csv' | 'file'。

Service 层规范 ​

业务逻辑、数据映射、前置校验统一放在 packages/server/src/services/{业务域}/ 下,目录与 src/routes/{业务域}/ 对齐。

层职责禁止事项
route handler读取 c.req.valid()、调用 service、返回 HTTP 响应、设置必要审计快照直接写业务规则、数据映射、DB 查询
service业务规则、数据映射、前置校验、复杂查询、事务、关联写操作;通过 currentUser() 获取登录用户c.json()、直接依赖 Hono Context、console.*

标准资源的 Service 由 defineCrudService(contract, { table, map, notFound, unique, tenant | scope, list, create, update, remove }) (lib/crud-service.ts)生成,类型全部从契约派生,可见范围只声明一次并对读写全部生效;行 → 实体映射用 entityMapper(xxxSchema, overrides?) / pickEntity(xxxSchema, row, overrides?)(lib/entity-map.ts)按契约实体 schema 投影。 常用命名(工厂产物以解构别名导出,显式 Service 沿用同名):

ts
export const mapXxx = entityMapper(xxxSchema);
export const xxxService = defineCrudService(xxxContract, { table: xxxs, map: mapXxx, notFound: 'XXX 不存在', list: (q) => ({ where: [...], orderBy: [...] }) });
export const { list: listXxxs, get: getXxx, ensure: ensureXxxExists, create: createXxx, update: updateXxx, remove: deleteXxx } = xxxService;

业务错误抛 HTTPException(statusCode, { message }),由 app.onError() 转为统一 JSON。唯一约束冲突使用 rethrowPgUniqueViolation(err, message, byConstraint?) 映射为 400。

响应实体 schema(契约单一来源) ​

响应实体形状定义在契约文件的 xxxSchema(.meta({ id: 'Xxx' }) 即 OpenAPI 组件名), type Xxx = z.infer<typeof xxxSchema> 是前后端共用的实体类型;服务端直接以契约 schema 作为响应 schema,实体只有这一份定义。

  • 组件名全局唯一,按「域 + 实体」命名(CmsContent / WikiComment)。
  • 精简变体用 xxxSchema.pick({...}),附带关联数据的详情用 xxxSchema.extend({...}),各自给独立的 id。
  • 请求体 schema 在 validation.ts(createXxxSchema / updateXxxSchema),契约只引用不复制。

常用错误码 ​

code含义
0成功
400参数校验失败或业务前置条件不满足
401未登录、token 无效或会话失效
403权限不足、账号禁用、功能授权不满足
404资源不存在
408请求处理超时(启用 REQUEST_TIMEOUT_MS 时)
409并发冲突、乐观锁重试耗尽
410导出文件等资源已过期
413请求体超出大小限制(启用 REQUEST_BODY_LIMIT 时)
423登录账号被锁定
429触发接口级限流
500服务端内部错误

路由组织 ​

  • 路由文件位于 packages/server/src/routes/{业务域}/,每个文件导出一个子路由器。
  • 每个业务域在 routes/{业务域}/index.ts 中用 defineRouteDomain 声明挂载清单。
  • routes/index.ts 的 ROUTE_DOMAINS 声明域顺序:ops → identity → member → platform → files → tasks → analytics → report → messaging → payment → open-platform → workflow → chat → mp → biz-demo → ai → cms → wiki。
  • src/app.ts 的 createApp() 按域装配常规 API,再挂载 /api/mastra/*,再注册 Swagger 文档和 fallback 路由。
  • CMS 前台 SSR 等兜底路由放在域的 fallback() 中,保证晚于全部 API 与文档路由。
  • 全量 method + path 由 src/app.contract.test.ts 快照锁定;增删接口需更新该快照。

数据删除与批量操作规范 ​

  • 单条删除:DELETE /api/resource/{id}。
  • 批量删除:DELETE /api/resource/batch 或按领域既有约定使用 POST /batch-delete,body 传 { ids: number[] }。
  • 批量修改状态:PUT /api/resource/batch-status,body 传 { ids: number[], status: 'enabled' | 'disabled' }。
  • DELETE /batch 必须注册在 DELETE /{id} 之前,避免被动态参数捕获。

文件上传 ​

标准文件上传由文件域提供。小文件使用 POST /api/files/upload(multipart/form-data);大文件使用上传会话与分片接口。文件下载、预览和业务附件关系由 packages/server/src/routes/files/ 与 services/files/ 收口。

健康检查 ​

GET /api/health 无需鉴权,返回服务状态、版本、运行时长,以及数据库 / Redis 连通性:

json
{
  "code": 0,
  "message": "success",
  "data": {
    "status": "ok",
    "version": "1.90.0",
    "uptimeSeconds": 12345,
    "checks": { "database": "ok", "redis": "ok" }
  }
}

Prometheus 指标 ​

GET /metrics 无需鉴权,返回 Prometheus 文本格式指标,不属于 OpenAPI 文档。指标来源包括:

  • @hono/prometheus HTTP RED 指标;
  • prom-client 默认进程指标;
  • registerZenithMetrics() 注册的 Zenith 业务 / 系统指标(CPU、内存、HTTP、WebSocket、DB、Redis 等)。

OpenTelemetry Trace ​

服务端通过 @hono/otel 接入 Hono 请求 Trace。OTEL_ENABLED=true 时启用;未显式设置 OTEL_ENABLED 但配置了 OTEL_EXPORTER_OTLP_TRACES_ENDPOINT 或 OTEL_EXPORTER_OTLP_ENDPOINT 时也会启用。采集请求 / 响应头包括 x-request-id、user-agent。

变量默认值说明
OTEL_ENABLEDfalse是否启用 Trace
OTEL_SERVICE_NAMEzenith-admin-server服务名
OTEL_SERVICE_VERSIONnpm package version服务版本
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT空OTLP traces 专用导出地址
OTEL_EXPORTER_OTLP_ENDPOINT空通用 OTLP 导出地址
OTEL_EXPORTER_OTLP_HEADERS空导出请求头

共享约定 ​

  • 类型、Zod schema、枚举和常量按域放到 @zenith/shared/{业务域} 子路径。
  • 禁止从 @zenith/shared 根入口导入。
  • 种子数据统一从 @zenith/shared/seed 导入。

Server-Timing 性能分析头 ​

SERVER_TIMING_ENABLED=true 时,服务端通过 hono/timing 为响应附加 Server-Timing 头。默认关闭。

http
Server-Timing: total;dur=45.2;desc="Total Response Time"

路由内部可使用 startTime(c, name) / endTime(c, name) 标记关键阶段。

请求防护 ​

服务端在 createApp() 中装配以下中间件:

能力实现说明
Request IDhono/request-id为请求设置 requestId 上下文
Trace IDrequestTraceMiddleware接收 X-Trace-Id(≤64 字符)或生成 UUID,并回写响应头
安全响应头hono/secure-headersAPI 场景下放开 CORP / COOP / X-Frame-Options 的不适用限制
压缩hono/compressWebSocket、文件、日志流、监控流、AI 流式、公开制品等长连接 / 二进制端点排除
CORShono/cors/api/mastra/* 反射 Origin 并允许 credentials,其余路径使用 CORS_ORIGIN
CSRFcreateCsrfMiddleware + hono/csrf来源检查、Bearer 机器调用及协议例外见 安全 · CSRF;拒绝返回明确的 403 文案
Body Limithono/body-limitREQUEST_BODY_LIMIT > 0 时启用,超限返回 413
Timeouthono/timeoutREQUEST_TIMEOUT_MS > 0 时对 /api/* 启用,WebSocket / 文件 / DB 管理 / 日志 / 监控流 / AI 流 / 应用发布制品 / */export 排除
IP 访问控制ipAccessMiddleware对 /api/* 生效
限流rate-limit.ts登录、验证码、注册 / 找回 / 重置密码和路径绑定限流
维护模式maintenanceMiddleware对 /api/* 生效,认证与公开维护接口除外
License 门控licenseFeatureGate挂载继承路由域的 licensing.feature;豁免需说明原因,见 功能授权

Built with VitePress for local documentation preview.