OpenAPI 文档

VextJS 内置 OpenAPI 文档自动生成功能。基于路由的 validatedocs 配置,框架自动生成 OpenAPI 3.0 规范的 JSON 文档,并提供 Scalar API Reference 在线查看和交互式调试。

快速开始

1. 启用 OpenAPI

在配置中开启 openapi.enabled

// src/config/default.ts
export default {
  port: 3000,
  openapi: {
    enabled: true,
  },
};

2. 在路由中添加文档信息

// src/routes/users.ts
import { defineRoutes } from 'vextjs';

export default defineRoutes((app) => {
  app.get('/', {
    validate: {
      query: {
        page: 'number:1-',
        limit: 'number:1-100',
      },
    },
    docs: {
      summary: '获取用户列表',
      description: '分页获取所有用户信息',
      tags: ['用户管理'],
    },
  }, async (req, res) => {
    const { page = 1, limit = 20 } = req.valid('query');
    const users = await app.services.user.findAll({ page, limit });
    res.json(users);
  });

  app.post('/', {
    validate: {
      body: {
        name: 'string:1-50!',
        email: 'email!',
        age: 'number:0-150?',
      },
    },
    middlewares: ['auth'],
    docs: {
      summary: '创建用户',
      tags: ['用户管理'],
    },
  }, async (req, res) => {
    const data = req.valid('body');
    const user = await app.services.user.create(data);
    res.json(user, 201);
  });
});

3. 访问文档

启动项目后,访问以下地址:

地址说明
http://localhost:3000/docsScalar API Reference 文档界面(含内置 Try it out)
http://localhost:3000/openapi.jsonOpenAPI JSON 规范文件

文档配置

全局配置

config/default.ts 中配置 OpenAPI 全局信息:

// src/config/default.ts
export default {
  openapi: {
    enabled: true,
    title: 'My App API',
    description: '我的应用程序 RESTful API 文档',
    version: '1.0.0',

    // Scalar 文档路径
    docsPath: '/docs',

    // OpenAPI JSON 路径
    specPath: '/openapi.json',

    // Scalar API Reference 配置
    scalar: {
      theme: 'default',     // 主题:'default' | 'moon' | 'purple' | 'solarized' | ...
      darkMode: false,       // 深色模式
      layout: 'modern',     // 布局:'modern'(三栏) | 'classic'(双栏)
      favicon: '/favicon.svg', // 文档页面图标
    },

    // API 服务器列表
    servers: [
      { url: 'http://localhost:3000', description: '本地开发' },
      { url: 'https://api.myapp.com', description: '生产环境' },
    ],

    // 标签定义(控制分组顺序和描述)
    tags: [
      { name: '用户管理', description: '用户 CRUD 操作' },
      { name: '订单管理', description: '订单相关接口' },
      { name: '系统', description: '系统级接口' },
    ],

    // 安全方案定义
    securitySchemes: {
      bearerAuth: {
        type: 'http',
        scheme: 'bearer',
        bearerFormat: 'JWT',
      },
      apiKeyAuth: {
        type: 'apiKey',
        in: 'header',
        name: 'X-API-Key',
      },
    },

    // 中间件名 → 安全方案映射
    guardSecurityMap: {
      auth: 'bearerAuth',
      'api-key': 'apiKeyAuth',
    },

    // 联系方式
    contact: {
      name: 'API Support',
      email: 'support@myapp.com',
      url: 'https://myapp.com/support',
    },

    // 许可证
    license: {
      name: 'MIT',
      url: 'https://opensource.org/licenses/MIT',
    },
  },
};

路由级文档配置

每个路由可以通过 options.docs 配置其 OpenAPI 文档信息:

app.post('/users', {
  validate: { ... },
  docs: {
    // 接口摘要(一句话描述)
    summary: '创建用户',

    // 详细描述(支持 Markdown)
    description: '创建一个新用户。\n\n**注意:** 邮箱必须唯一。',

    // 标签分组(默认从路由文件路径推断)
    tags: ['用户管理'],

    // 操作标识(全局唯一,默认自动推断)
    operationId: 'createUser',

    // 是否已废弃
    deprecated: false,

    // 是否从文档中隐藏
    hidden: false,

    // 安全方案覆盖
    security: [{ bearerAuth: [] }],

    // 自定义响应定义
    responses: {
      201: {
        description: '创建成功',
        schema: { id: 'string', name: 'string', email: 'email' },
      },
      409: {
        description: '邮箱已存在',
      },
    },

    // 自定义扩展字段(x- 前缀)
    extensions: {
      'x-internal': true,
      'x-rate-limit': '10/min',
    },
  },
}, handler);

docs 配置详解

summary — 接口摘要

一句话描述接口功能,显示在文档 UI 的接口列表中:

docs: { summary: '获取用户列表' }

description — 详细描述

支持 Markdown 格式的详细说明,展开接口时显示:

docs: {
  summary: '创建用户',
  description: `
创建一个新用户账户。

**前置条件:**
- 需要管理员权限
- 邮箱地址必须唯一

**返回值:**
- 成功时返回新创建的用户对象
- 邮箱冲突时返回 409 错误
  `,
}

tags — 标签分组

控制接口在文档中的分组。如果不指定,框架会从路由文件路径自动推断:

src/routes/users.ts      → 默认 tag: 'users'
src/routes/admin/users.ts → 默认 tag: 'admin'
// 手动指定(覆盖自动推断)
docs: { tags: ['用户管理', '管理后台'] }

operationId — 操作标识

全局唯一的操作标识符。如果不指定,框架自动推断:

POST /users       → operationId: 'createUsers'
GET  /users       → operationId: 'getUsers'
GET  /users/:id   → operationId: 'getUsersById'
PUT  /users/:id   → operationId: 'updateUsersById'
DELETE /users/:id → operationId: 'deleteUsersById'
// 手动指定
docs: { operationId: 'createNewUser' }

hidden — 隐藏路由

不希望出现在文档中的路由(如内部接口):

app.get('/internal/metrics', {
  docs: { hidden: true },
}, handler);

app.get('/_health', {
  docs: { hidden: true },
}, handler);

deprecated — 标记废弃

标记接口为已废弃,在文档中会有删除线和废弃提示:

app.get('/v1/users', {
  docs: {
    summary: '获取用户列表(已废弃)',
    description: '请使用 `/v2/users` 替代',
    deprecated: true,
  },
}, handler);

security — 安全方案

默认情况下,框架会从路由的 middlewares 自动推断安全方案。通过 guardSecurityMap 配置中间件名到安全方案的映射:

// config/default.ts
export default {
  openapi: {
    securitySchemes: {
      bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
    },
    guardSecurityMap: {
      auth: 'bearerAuth',  // middlewares 中包含 'auth' → 映射为 bearerAuth
    },
  },
};

路由使用 middlewares: ['auth'] 时,OpenAPI 文档自动标注需要 Bearer Token 认证。

手动覆盖:

// 无需认证(即使有 auth 中间件)
docs: { security: [] }

// 指定特定安全方案
docs: { security: [{ apiKeyAuth: [] }] }

responses — 响应定义

自定义路由的响应文档。key 为 HTTP 状态码:

docs: {
  responses: {
    200: {
      description: '成功返回用户列表',
      schema: {
        id: 'string',
        name: 'string',
        email: 'email',
        role: 'admin|user',
      },
    },
    401: {
      description: '未认证',
    },
    403: {
      description: '权限不足',
    },
    500: {
      description: '服务器内部错误',
    },
  },
}

响应 schema 使用与 validate 相同的 DSL 语法,自动转换为 JSON Schema。

响应示例

docs: {
  responses: {
    200: {
      description: '用户详情',
      example: {
        id: '550e8400-e29b-41d4-a716-446655440000',
        name: 'Alice',
        email: 'alice@example.com',
        role: 'admin',
      },
    },
    404: {
      description: '用户不存在',
      example: {
        code: 40001,
        message: '用户不存在',
        requestId: 'xxx',
      },
    },
  },
}

多响应示例

docs: {
  responses: {
    200: {
      description: '用户详情',
      examples: {
        admin: {
          summary: '管理员用户',
          value: { id: '1', name: 'Admin', role: 'admin' },
        },
        regular: {
          summary: '普通用户',
          value: { id: '2', name: 'User', role: 'user' },
        },
      },
    },
  },
}

自定义 Content-Type

docs: {
  responses: {
    200: {
      description: 'CSV 导出文件',
      contentType: 'text/csv',
    },
  },
}

响应头

docs: {
  responses: {
    200: {
      description: '成功',
      headers: {
        'X-Total-Count': {
          description: '总记录数',
          schema: { type: 'integer' },
        },
        'X-Page': {
          description: '当前页码',
          schema: { type: 'integer' },
        },
      },
    },
  },
}

validate 与文档的自动联动

路由中的 validate 规则会自动映射到 OpenAPI 文档,无需重复编写:

app.get('/users', {
  validate: {
    query: {
      page: 'number:1-',
      limit: 'number:1-100',
      status: 'active|inactive|banned',
      keyword: 'string?',
    },
  },
  docs: { summary: '获取用户列表' },
}, handler);

自动生成的 OpenAPI 参数:

参数位置类型约束
pagequeryintegerminimum: 1
limitqueryintegerminimum: 1, maximum: 100
statusquerystringenum: ["active", "inactive", "banned"]
keywordquerystring

validate.body 的规则自动映射为 requestBody(JSON schema):

app.post('/users', {
  validate: {
    body: {
      name: 'string:1-50!',
      email: 'email!',
      age: 'number:0-150?',
    },
  },
}, handler);

生成的 requestBody schema:

{
  "type": "object",
  "required": ["name", "email"],
  "properties": {
    "name": { "type": "string", "minLength": 1, "maxLength": 50 },
    "email": { "type": "string", "format": "email" },
    "age": { "type": "number", "minimum": 0, "maximum": 150 }
  }
}

按环境控制

建议在开发环境启用文档,生产环境关闭:

// src/config/default.ts
export default {
  openapi: {
    enabled: true,
    title: 'My App API',
    scalar: {
      theme: 'default',
      favicon: '/favicon.svg',
    },
  },
};
// src/config/production.ts
export default {
  openapi: {
    enabled: false,   // 生产环境关闭文档
  },
};

如果生产环境需要保留 API 文档(只读参考):

// src/config/production.ts
export default {
  openapi: {
    enabled: true,
    scalar: {
      darkMode: false,
    },
  },
};

自定义文档路径

export default {
  openapi: {
    enabled: true,
    docsPath: '/api-docs',       // 文档: http://localhost:3000/api-docs
    specPath: '/api/spec.json',  // JSON: http://localhost:3000/api/spec.json
  },
};

导入外部 OpenAPI

Scalar 支持同时加载多个 OpenAPI 文档,在文档页面顶部显示切换器。通过 scalar.sources 配置:

export default {
  openapi: {
    enabled: true,
    scalar: {
      sources: [
        // 框架自动生成的 spec 会作为第一个 source 注入(无需手动添加)
        { title: 'Partner API', url: 'https://partner.example.com/openapi.json', slug: 'partner' },
        { title: 'Payment API', url: 'https://pay.example.com/v1/openapi.json', slug: 'payment' },
      ],
    },
  },
};

每个 source 支持以下字段:

字段类型说明
titlestring文档标题(显示在切换器中)
urlstringOpenAPI 规范 URL(远程或本地端点)
contentstringOpenAPI 规范内联 JSON 字符串(与 url 二选一)
slugstringURL slug 标识(如 /docs#/slug
自动注入

当配置了 sources 时,框架自动生成的 /openapi.json 会作为第一个 source 注入(除非 sources 中已包含相同路径),无需手动重复声明。

也可以通过 content 内联提供规范(适合小型/固定的 spec):

scalar: {
  sources: [
    {
      title: 'Mock API',
      content: JSON.stringify({
        openapi: '3.0.0',
        info: { title: 'Mock', version: '1.0.0' },
        paths: {},
      }),
      slug: 'mock',
    },
  ],
}

自定义 CDN / 本地资产

默认情况下,Scalar 通过 jsDelivr CDN 加载 JS 文件。如果需要在内网、离线环境或需要版本锁定的场景下使用,可以通过 scalar.cdnUrl 自定义加载地址:

export default {
  openapi: {
    enabled: true,
    scalar: {
      // 方式 1:锁定特定版本
      cdnUrl: 'https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.25.0',

      // 方式 2:使用内网 CDN 镜像
      // cdnUrl: 'https://static.internal.com/libs/scalar-api-reference.js',

      // 方式 3:使用本地静态文件(需配合静态文件服务)
      // cdnUrl: '/static/scalar-api-reference.js',
    },
  },
};
离线部署

对于完全离线的环境,你需要:

  1. 从 npm 下载 @scalar/api-reference
  2. 将 JS 文件托管到内网静态服务器或应用的 public 目录
  3. cdnUrl 指向该地址 :::

与第三方工具集成

导出 OpenAPI 规范

访问 http://localhost:3000/openapi.json 获取完整的 OpenAPI 3.0 JSON 文件,可用于:

  • Postman — 导入 API 集合
  • Insomnia — 导入 API 工作区
  • 代码生成 — 使用 openapi-generator 生成客户端 SDK
  • API 网关 — 导入到 Kong、AWS API Gateway 等
  • 文档平台 — 导入到 Stoplight、ReadMe 等

示例:生成 TypeScript 客户端

npx openapi-generator-cli generate \
  -i http://localhost:3000/openapi.json \
  -g typescript-fetch \
  -o ./generated/api-client

文档最佳实践

1. 始终提供 summary

summary 是接口在文档列表中最重要的标识,应简洁明了:

// ✅ 好的 summary
docs: { summary: '获取用户列表' }
docs: { summary: '创建订单' }
docs: { summary: '上传用户头像' }

// ❌ 不好的 summary
docs: { summary: '这个接口用于获取系统中所有用户的列表数据' }  // 太长
docs: { summary: 'GET users' }  // 没有价值

2. 使用一致的标签

统一使用中文或英文标签,并在全局 tags 中预定义顺序和描述:

// ✅ 在 config 中统一定义
openapi: {
  tags: [
    { name: '认证', description: '登录、注册、Token 管理' },
    { name: '用户', description: '用户 CRUD' },
    { name: '订单', description: '订单管理' },
    { name: '系统', description: '健康检查、配置信息' },
  ],
}

3. 为错误响应添加文档

常见的错误码应在 responses 中说明:

docs: {
  summary: '创建用户',
  responses: {
    201: { description: '创建成功' },
    400: { description: '请求参数错误' },
    401: { description: '未认证' },
    409: { description: '邮箱已存在' },
    422: { description: '参数校验失败' },
  },
}

4. 隐藏内部接口

框架内部或运维使用的接口应标记为 hidden

// 健康检查、指标、调试接口等
app.get('/health', { docs: { hidden: true } }, handler);
app.get('/metrics', { docs: { hidden: true } }, handler);
app.get('/debug/config', { docs: { hidden: true } }, handler);

5. 善用 deprecated

API 版本迭代时,使用 deprecated 而非直接删除旧接口:

// v1 接口标记废弃
app.get('/v1/users', {
  docs: {
    summary: '获取用户列表 (v1)',
    deprecated: true,
    description: '此接口已废弃,请使用 `GET /v2/users`',
  },
}, handler);

// v2 新接口
app.get('/v2/users', {
  docs: {
    summary: '获取用户列表',
    tags: ['用户 v2'],
  },
}, handler);

多级目录示例

VextJS 的文件路由支持多层嵌套目录,每一级目录自动映射为 URL 路径段。配合 tags 分组,多级路由在 Scalar 文档页面中自动归类展示。

目录结构

src/routes/
├── index.ts                          # → /
├── api/
   └── v1/
       ├── index.ts                  # → /api/v1
       ├── users.ts                  # → /api/v1/users
       ├── users/
   └── [id]/
       └── orders.ts         # → /api/v1/users/:id/orders
       └── admin/
           ├── dashboard.ts          # → /api/v1/admin/dashboard
           └── users.ts              # → /api/v1/admin/users
└── webhooks/
    └── stripe.ts                     # → /webhooks/stripe

路径映射对照

文件路径URL 前缀说明
routes/index.ts/根路由(健康检查)
routes/api/v1/index.ts/api/v1API 版本入口
routes/api/v1/users.ts/api/v1/users用户公开接口
routes/api/v1/users/[id]/orders.ts/api/v1/users/:id/orders用户订单(动态参数嵌套)
routes/api/v1/admin/dashboard.ts/api/v1/admin/dashboard管理后台仪表盘
routes/api/v1/admin/users.ts/api/v1/admin/users管理后台用户管理
routes/webhooks/stripe.ts/webhooks/stripeStripe 回调

全局 tags 定义

在配置中预定义标签,Scalar 文档会按标签分组展示:

// src/config/default.ts
export default {
  port: 3000,
  openapi: {
    enabled: true,
    title: 'My App API',
    version: '2.0.0',
    tags: [
      { name: 'v1/用户', description: '用户公开接口' },
      { name: 'v1/用户订单', description: '用户关联订单' },
      { name: 'v1/管理后台', description: '管理员专用接口' },
      { name: 'Webhook', description: '第三方回调' },
    ],
  },
};

各路由文件

routes/api/v1/users.ts — 用户公开接口

// src/routes/api/v1/users.ts
import { defineRoutes } from 'vextjs';

export default defineRoutes((app) => {
  // GET /api/v1/users → 用户列表
  app.get('/', {
    validate: {
      query: {
        page: 'number:1-',
        limit: 'number:1-50',
        role: 'admin|user?',
      },
    },
    docs: {
      summary: '获取用户列表',
      tags: ['v1/用户'],
    },
  }, async (req, res) => {
    const filters = req.valid('query');
    const users = await app.services.user.findAll(filters);
    res.json(users);
  });

  // GET /api/v1/users/:id → 用户详情
  app.get('/:id', {
    validate: { param: { id: 'string!' } },
    docs: {
      summary: '获取用户详情',
      tags: ['v1/用户'],
      responses: {
        200: { description: '用户信息' },
        404: { description: '用户不存在' },
      },
    },
  }, async (req, res) => {
    const { id } = req.valid('param');
    const user = await app.services.user.findById(id);
    if (!user) app.throw(404, 'user.not_found');
    res.json(user);
  });
});

routes/api/v1/users/[id]/orders.ts — 用户订单(多级动态参数)

// src/routes/api/v1/users/[id]/orders.ts
import { defineRoutes } from 'vextjs';

export default defineRoutes((app) => {
  // GET /api/v1/users/:id/orders → 该用户的订单列表
  app.get('/', {
    validate: {
      param: { id: 'string!' },
      query: {
        status: 'pending|paid|shipped|completed?',
        limit: 'number:1-100',
      },
    },
    docs: {
      summary: '获取用户订单列表',
      description: '获取指定用户的所有订单,支持按状态筛选。',
      tags: ['v1/用户订单'],
      responses: {
        200: { description: '订单列表' },
        404: { description: '用户不存在' },
      },
    },
  }, async (req, res) => {
    const { id } = req.valid('param');
    const filters = req.valid('query');
    const orders = await app.services.order.findByUserId(id, filters);
    res.json(orders);
  });

  // GET /api/v1/users/:id/orders/:orderId → 订单详情
  app.get('/:orderId', {
    validate: {
      param: { id: 'string!', orderId: 'string!' },
    },
    docs: {
      summary: '获取订单详情',
      tags: ['v1/用户订单'],
    },
  }, async (req, res) => {
    const { id, orderId } = req.valid('param');
    const order = await app.services.order.findOne(id, orderId);
    if (!order) app.throw(404, 'order.not_found');
    res.json(order);
  });
});

routes/api/v1/admin/dashboard.ts — 管理后台

// src/routes/api/v1/admin/dashboard.ts
import { defineRoutes } from 'vextjs';

export default defineRoutes((app) => {
  // GET /api/v1/admin/dashboard/stats → 统计数据
  app.get('/stats', {
    middlewares: ['auth', { name: 'check-role', options: { roles: ['admin'] } }],
    docs: {
      summary: '获取仪表盘统计',
      tags: ['v1/管理后台'],
      responses: {
        200: {
          description: '统计数据',
          example: {
            totalUsers: 1024,
            activeToday: 256,
            totalOrders: 8192,
            revenue: 99999.99,
          },
        },
        401: { description: '未认证' },
        403: { description: '权限不足' },
      },
    },
  }, async (_req, res) => {
    const stats = await app.services.dashboard.getStats();
    res.json(stats);
  });
});

routes/api/v1/admin/users.ts — 管理后台用户管理

// src/routes/api/v1/admin/users.ts
import { defineRoutes } from 'vextjs';

export default defineRoutes((app) => {
  // GET /api/v1/admin/users → 管理员查看所有用户
  app.get('/', {
    middlewares: ['auth', { name: 'check-role', options: { roles: ['admin'] } }],
    validate: {
      query: {
        page: 'number:1-',
        limit: 'number:1-100',
        status: 'active|banned|suspended?',
      },
    },
    docs: {
      summary: '管理员查看用户列表',
      description: '管理员专用,支持按用户状态筛选,返回完整用户信息。',
      tags: ['v1/管理后台'],
    },
  }, async (req, res) => {
    const filters = req.valid('query');
    const users = await app.services.user.adminFindAll(filters);
    res.json(users);
  });

  // PATCH /api/v1/admin/users/:id/ban → 封禁用户
  app.patch('/:id/ban', {
    middlewares: ['auth', { name: 'check-role', options: { roles: ['admin'] } }],
    validate: {
      param: { id: 'string!' },
      body: { reason: 'string:1-500!' },
    },
    docs: {
      summary: '封禁用户',
      tags: ['v1/管理后台'],
      responses: {
        200: { description: '封禁成功' },
        404: { description: '用户不存在' },
      },
    },
  }, async (req, res) => {
    const { id } = req.valid('param');
    const { reason } = req.valid('body');
    await app.services.user.ban(id, reason);
    res.json({ success: true });
  });
});

routes/webhooks/stripe.ts — 第三方回调

// src/routes/webhooks/stripe.ts
import { defineRoutes } from 'vextjs';

export default defineRoutes((app) => {
  // POST /webhooks/stripe → Stripe 事件回调
  app.post('/', {
    validate: {
      header: { 'stripe-signature': 'string!' },
    },
    docs: {
      summary: 'Stripe Webhook 回调',
      tags: ['Webhook'],
      description: '接收 Stripe 支付事件通知。需要验证签名。',
      responses: {
        200: { description: '处理成功' },
        400: { description: '签名验证失败' },
      },
    },
  }, async (req, res) => {
    const signature = req.valid('header')['stripe-signature'];
    await app.services.payment.handleStripeWebhook(req.body, signature);
    res.json({ received: true });
  });
});

生成的 OpenAPI 路径

以上目录结构最终自动生成以下 OpenAPI 路径,在 Scalar 文档中按 tags 分组展示:

OpenAPI 路径方法Tag来源文件
/api/v1/usersGETv1/用户api/v1/users.ts
/api/v1/users/{id}GETv1/用户api/v1/users.ts
/api/v1/users/{id}/ordersGETv1/用户订单api/v1/users/[id]/orders.ts
/api/v1/users/{id}/orders/{orderId}GETv1/用户订单api/v1/users/[id]/orders.ts
/api/v1/admin/dashboard/statsGETv1/管理后台api/v1/admin/dashboard.ts
/api/v1/admin/usersGETv1/管理后台api/v1/admin/users.ts
/api/v1/admin/users/{id}/banPATCHv1/管理后台api/v1/admin/users.ts
/webhooks/stripePOSTWebhookwebhooks/stripe.ts

:::tip 多级目录最佳实践

  • 用目录层级表达 URL 结构api/v1/admin/ 自动映射为 /api/v1/admin/ 前缀,无需手动拼接
  • 动态参数用 [param] 目录users/[id]/orders.ts 自动变为 /users/:id/orders,文件内的 param 校验会出现在 OpenAPI 文档中
  • tags 统一管理:在全局配置中预定义 tags,各路由文件通过 docs.tags 引用,Scalar 文档按标签分组
  • 文件名即路由:无需 app.group() 或手动注册路由前缀,目录结构就是路由结构 :::

标签分组(x-tagGroups)

OpenAPI 3.x 规范的 tags一维扁平列表,不原生支持嵌套层级。当路由数量较多时,所有 tags 在 Scalar 文档侧边栏中平铺并列,不便于导航。

VextJS 通过 Scalar 支持的 x-tagGroups 扩展 实现两级导航:将多个 tags 归入更高级别的分组(group),在 Scalar 侧边栏中展示为可折叠的分组层级。

自动推断(默认行为)

当路由文件分布在多个顶层目录时,框架自动推断 x-tagGroups

  • 提取每条路由文件在 routes/ 之后的第一层目录名作为分组名(首字母大写)
  • 直接位于 routes/ 下的文件归入 "General" 分组
  • 如果所有路由都在同一个分组中,则不生成 x-tagGroups(避免冗余)
src/routes/
├── health.ts              → 分组: General
├── api/
│   ├── users.ts           → 分组: Api
│   └── orders.ts          → 分组: Api
├── admin/
│   ├── dashboard.ts       → 分组: Admin
│   └── users.ts           → 分组: Admin
└── webhooks/
    └── stripe.ts          → 分组: Webhooks

生成的 x-tagGroups

{
  "x-tagGroups": [
    { "name": "Admin", "tags": ["admin-dashboard", "admin-users"] },
    { "name": "Api", "tags": ["api-orders", "api-users"] },
    { "name": "Webhooks", "tags": ["webhooks-stripe"] },
    { "name": "General", "tags": ["health"] }
  ]
}

:::tip 分组按字母排序,General 始终排在最后。同一分组内的 tags 也按字母排序且自动去重。

手动配置(覆盖自动推断)

如果自动推断的分组不满足需求,可以在配置中显式指定 tagGroups

// src/config/app.ts
export default {
  port: 3000,
  openapi: {
    enabled: true,
    title: 'My API',
    version: '1.0.0',

    // 手动配置标签分组
    tagGroups: [
      {
        name: 'User API',
        tags: ['users', 'user-profile', 'user-orders'],
      },
      {
        name: 'Administration',
        tags: ['admin-dashboard', 'admin-users'],
      },
      {
        name: 'Integration',
        tags: ['webhooks'],
      },
    ],

    // tags 定义(可选,提供描述信息)
    tags: [
      { name: 'users', description: '用户公开接口' },
      { name: 'user-profile', description: '用户个人资料' },
      { name: 'user-orders', description: '用户订单' },
      { name: 'admin-dashboard', description: '管理后台仪表盘' },
      { name: 'admin-users', description: '管理后台用户管理' },
      { name: 'webhooks', description: '第三方回调' },
    ],
  },
};
Warning

配置了 tagGroups 后,框架不再自动推断,直接使用用户配置。请确保所有 tags 都被覆盖到至少一个分组中,否则未分组的 tags 在 Scalar 中可能不显示。

效果对比

无 x-tagGroups有 x-tagGroups
所有 tags 平铺在侧边栏tags 按分组折叠展示
users / admin-dashboard / orders / webhooks 并列User API ▸ users, orders / Admin ▸ admin-dashboard
适合少量路由适合路由数量较多的项目

与热重载的兼容性

在 dev 模式下,热重载(soft reload)会自动重新生成 x-tagGroups

  1. 路由文件变更 → 触发热重载
  2. 创建新的 adapter 实例
  3. 重新加载路由 + 收集路由元信息
  4. 重新生成 OpenAPI spec(含 x-tagGroups
  5. 在新 adapter 上重新注册 /docs/openapi.json 端点

无需重启 dev server,刷新文档页面即可看到更新后的分组。

完整示例

// src/routes/orders.ts
import { defineRoutes } from 'vextjs';

export default defineRoutes((app) => {
  // 获取订单列表
  app.get('/', {
    validate: {
      query: {
        page: 'number:1-',
        limit: 'number:1-50',
        status: 'pending|paid|shipped|completed|cancelled',
        startDate: 'date?',
        endDate: 'date?',
      },
    },
    middlewares: ['auth'],
    docs: {
      summary: '获取订单列表',
      description: '分页获取当前用户的订单列表,支持按状态和日期范围筛选。',
      tags: ['订单'],
      responses: {
        200: {
          description: '订单列表',
          headers: {
            'X-Total-Count': {
              description: '总订单数',
              schema: { type: 'integer' },
            },
          },
        },
      },
    },
  }, async (req, res) => {
    const filters = req.valid('query');
    const orders = await app.services.order.findAll(filters);
    res.json(orders);
  });

  // 创建订单
  app.post('/', {
    validate: {
      body: {
        productId: 'string!',
        quantity: 'number:1-99!',
        shippingAddress: 'string:1-200!',
        couponCode: 'string?',
      },
    },
    middlewares: ['auth'],
    docs: {
      summary: '创建订单',
      tags: ['订单'],
      responses: {
        201: {
          description: '订单创建成功',
          example: {
            orderId: 'ord_abc123',
            status: 'pending',
            total: 99.99,
          },
        },
        400: { description: '库存不足或优惠券无效' },
        401: { description: '未认证' },
      },
    },
  }, async (req, res) => {
    const data = req.valid('body');
    const order = await app.services.order.create(data);
    res.json(order, 201);
  });

  // 取消订单
  app.post('/:id/cancel', {
    validate: {
      param: { id: 'string!' },
      body: { reason: 'string:1-500?' },
    },
    middlewares: ['auth'],
    docs: {
      summary: '取消订单',
      tags: ['订单'],
      responses: {
        200: { description: '取消成功' },
        400: { description: '订单状态不允许取消' },
        404: { description: '订单不存在' },
      },
    },
  }, async (req, res) => {
    const { id } = req.valid('param');
    const { reason } = req.valid('body');
    await app.services.order.cancel(id, reason);
    res.json({ success: true });
  });
});

下一步

  • 了解 参数校验 的 DSL 语法如何映射到 OpenAPI
  • 学习 配置 中 OpenAPI 的完整选项
  • 查看 Adapter 架构 了解不同 Adapter 下的文档行为
  • 探索 测试 如何验证 API 文档的准确性