路由定义

本页详细介绍 VextJS 的路由定义 API,包括 defineRoutes、路由选项、参数校验、中间件引用和文档配置。

defineRoutes

defineRoutes 是创建路由文件的核心函数。它接收一个工厂回调,在回调中通过 app 对象注册路由。

import { defineRoutes } from 'vextjs';

export default defineRoutes((app) => {
  app.get('/hello', async (req, res) => {
    res.json({ message: 'Hello World' });
  });
});

函数签名

function defineRoutes(factory: RouteFactory): RouteDefinition;

type RouteFactory = (app: VextApp) => void;

工作原理

  1. defineRoutes(factory) 被调用时,内部创建一个 collector(路由收集器)
  2. factory(collector) 被执行,用户代码中的 app.get/post/... 实际调用 collector 的方法
  3. 每条路由被推入内部的 routes 数组
  4. 返回 RouteDefinition 对象
  5. router-loader 扫描 src/routes/ 目录,对每个文件的 default export 调用 register() 注册到底层适配器
Tip

在 factory 回调中,app 不仅有 HTTP 方法(get/post/put/...),还可以访问 app.servicesapp.configapp.throwapp.logger 等完整能力。这些属性由 router-loader 在执行 factory 前注入。


路由注册语法

VextJS 支持三段式两段式两种路由注册语法。

三段式(推荐)

app.method(path, options, handler)

带有 options 配置的完整语法,支持参数校验、中间件引用、文档配置等:

export default defineRoutes((app) => {
  app.post('/users', {
    validate: {
      body: { name: 'string:1-50', email: 'email' },
    },
    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);
  });
});

两段式

app.method(path, handler)

options 的简化语法,适用于不需要校验、中间件或文档配置的简单路由:

export default defineRoutes((app) => {
  app.get('/health', async (_req, res) => {
    res.json({ status: 'ok' });
  });
});

支持的 HTTP 方法

方法说明
app.get(path, ...)GET 请求
app.post(path, ...)POST 请求
app.put(path, ...)PUT 请求
app.patch(path, ...)PATCH 请求
app.delete(path, ...)DELETE 请求
app.head(path, ...)HEAD 请求
app.options(path, ...)OPTIONS 请求

路由路径

静态路径

app.get('/users', handler);
app.get('/users/profile', handler);

动态参数

使用 :paramName 定义动态路径参数,通过 req.paramsreq.valid('param') 访问:

app.get('/users/:id', {
  validate: {
    param: { id: 'string:1-' },
  },
}, async (req, res) => {
  const { id } = req.valid('param');
  const user = await app.services.user.findById(id);
  res.json(user);
});

通配符

app.get('/files/*', async (req, res) => {
  // req.params['*'] 包含通配符匹配的部分
  res.json({ path: req.params['*'] });
});

文件路由映射

路由文件的目录路径自动映射为 URL 前缀:

文件路径URL 前缀示例
src/routes/users.ts/usersapp.get('/list')GET /users/list
src/routes/api/orders.ts/api/ordersapp.post('/')POST /api/orders
src/routes/index.ts/app.get('/health')GET /health
Tip

路由文件中注册的 path相对子路径,框架自动拼接文件路径前缀。例如 src/routes/users.ts 中的 app.get('/:id') 最终注册为 GET /users/:id


RouteOptions

路由三段式语法的第二个参数,声明式配置对象。

interface RouteOptions {
  validate?: {
    query?: Record<string, unknown>;
    body?: Record<string, unknown>;
    param?: Record<string, unknown>;
    header?: Record<string, unknown>;
  };
  middlewares?: VextMiddlewareRef[];
  docs?: RouteDocsConfig;
  override?: {
    rateLimit?: { max?: number; window?: number; keyBy?: string } | false;
    timeout?: number;
    maxBodySize?: string | number;
    cors?: VextCorsConfig;
  };
}

完整示例

app.put('/users/:id', {
  validate: {
    param: { id: 'string:1-' },
    body: {
      name: 'string:1-50',
      email: 'email',
      age: 'number:0-200?',
    },
  },
  middlewares: ['auth', { name: 'cache', options: { ttl: 0 } }],
  docs: {
    summary: '更新用户',
    tags: ['用户'],
    responses: {
      200: { description: '更新成功' },
      404: { description: '用户不存在' },
    },
  },
  override: {
    rateLimit: { max: 10, window: 60 },
    maxBodySize: '5mb',
  },
}, handler);

validate

声明式参数校验,基于 schema-dsl DSL 语法。框架自动在 handler 执行前进行校验,校验失败返回 400 错误。

校验位置

位置数据源说明
paramreq.params路径动态参数(如 /:id
queryreq.queryURL 查询参数
headerreq.headers请求头
bodyreq.body请求体

校验执行顺序paramqueryheaderbody

基本用法

app.get('/users', {
  validate: {
    query: {
      page: 'number:1-',      // 大于等于 1 的数字
      limit: 'number:1-100',   // 1 到 100 之间的数字
      keyword: 'string?',     // 可选字符串
    },
  },
}, async (req, res) => {
  const { page, limit, keyword } = req.valid('query');
  // page: number, limit: number, keyword: string | undefined
});

DSL 语法速查

DSL说明示例
'string'必填字符串name: 'string'
'string:1-50'长度 1-50 的字符串name: 'string:1-50'
'string?'可选字符串nickname: 'string?'
'number'必填数字age: 'number'
'number:0-'大于等于 0 的数字page: 'number:0-'
'number:1-100'1 到 100 之间的数字limit: 'number:1-100'
'boolean'必填布尔值active: 'boolean'
'email'邮箱格式email: 'email'
'url'URL 格式website: 'url'
'date'日期格式birthday: 'date'
'uuid'UUID 格式id: 'uuid'
'enum:a,b,c'枚举值status: 'enum:active,inactive'
'array'数组tags: 'array'
'object'对象metadata: 'object'
Tip

schema-dsl 会自动做类型转换。例如查询参数 ?page=2 中的 '2'(字符串)会被自动转换为 2(数字),前提是 schema 声明为 'number' 类型。

获取校验后数据

使用 req.valid(location) 获取校验并类型转换后的数据:

app.post('/users', {
  validate: {
    body: { name: 'string:1-50', email: 'email' },
    query: { notify: 'boolean?' },
  },
}, async (req, res) => {
  const body = req.valid('body');    // { name: string, email: string }
  const query = req.valid('query');  // { notify?: boolean }
  // ...
});

可以通过泛型获得更精确的类型提示:

interface CreateUserBody {
  name: string;
  email: string;
}

const body = req.valid<CreateUserBody>('body');
// body.name  → IDE 知道是 string
// body.email → IDE 知道是 string

校验失败响应

校验失败时框架自动返回 400 状态码:

{
  "code": -1,
  "message": "Validation failed",
  "errors": [
    { "field": "email", "message": "must be a valid email address" },
    { "field": "name", "message": "length must be between 1 and 50" }
  ],
  "requestId": "550e8400-e29b-41d4-a716-446655440000"
}

middlewares

路由级中间件引用。引用的中间件必须先在 config.middlewares 白名单中声明。

字符串引用

app.get('/profile', {
  middlewares: ['auth'],
}, handler);

对象引用(带配置覆盖)

app.get('/admin/users', {
  middlewares: [
    'auth',
    { name: 'role', options: { required: 'admin' } },
  ],
}, handler);

VextMiddlewareRef 类型

type VextMiddlewareRef = string | { name: string; options?: unknown };

执行顺序

路由级中间件在全局中间件之后handler 之前执行:

请求 → [全局中间件链] → [路由级中间件] → [validate 中间件] → handler → 响应

配置白名单

路由中引用的中间件必须在配置文件中声明:

// src/config/default.ts
export default {
  middlewares: [
    { name: 'auth' },
    { name: 'role', options: { required: 'user' } },
    { name: 'cache', options: { ttl: 300 } },
  ],
};
// src/middlewares/auth.ts
import { defineMiddleware } from 'vextjs';

export default defineMiddleware(async (req, _res, next) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) {
    req.app.throw(401, '未提供认证令牌');
  }
  // 验证 token...
  req.user = decoded;
  await next();
});
Warning

引用未在白名单中声明的中间件会在启动时抛出错误:

[vextjs] Route GET "/profile" references middleware "auth" which is not
registered in config.middlewares whitelist.

docs

OpenAPI 文档配置,控制路由在自动生成的 API 文档中的展示方式。

RouteDocsConfig

interface RouteDocsConfig {
  summary?: string;
  description?: string;
  tags?: string[];
  operationId?: string;
  hidden?: boolean;
  deprecated?: boolean;
  security?: Array<Record<string, string[]>>;
  extensions?: Record<string, unknown>;
  responses?: Record<string | number, ResponseConfig>;
}

字段说明

字段类型默认值说明
summarystring接口一句话摘要
descriptionstring接口详细描述(支持 Markdown)
tagsstring[]从路由文件路径推断标签分组
operationIdstring自动推断操作标识(全局唯一)
hiddenbooleanfalse是否从文档中隐藏
deprecatedbooleanfalse是否标记为已废弃
securityarray从 middlewares 推断安全方案覆盖
extensionsobject自定义 x-* 扩展字段
responsesobject响应定义

完整示例

app.post('/users', {
  validate: {
    body: {
      name: 'string:1-50',
      email: 'email',
      role: 'enum:admin,user?',
    },
  },
  middlewares: ['auth'],
  docs: {
    summary: '创建用户',
    description: '创建一个新用户账号。需要管理员权限。',
    tags: ['用户管理'],
    operationId: 'createUser',
    responses: {
      201: {
        description: '用户创建成功',
        schema: {
          id: 'string',
          name: 'string',
          email: 'email',
          createdAt: 'date',
        },
        example: {
          id: 'usr_abc123',
          name: 'Alice',
          email: 'alice@example.com',
          createdAt: '2026-01-01T00:00:00Z',
        },
      },
      400: { description: '请求参数校验失败' },
      401: { description: '未认证' },
      409: { description: '邮箱已注册' },
    },
  },
}, handler);

operationId 自动推断

未指定 operationId 时,框架根据 HTTP 方法和路径自动生成:

方法 + 路径推断的 operationId
GET /usersgetUsers
POST /userscreateUsers
GET /users/:idgetUsersById
PUT /users/:idupdateUsersById
DELETE /users/:iddeleteUsersById

隐藏路由

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

标记废弃

app.get('/v1/users', {
  docs: {
    deprecated: true,
    description: '已废弃,请使用 /v2/users',
  },
}, handler);

安全方案覆盖

默认情况下,安全方案从 middlewares 自动推断(通过 config.openapi.guardSecurityMap 映射)。可手动覆盖:

// 显式声明需要 bearerAuth
app.get('/secure', {
  docs: {
    security: [{ bearerAuth: [] }],
  },
}, handler);

// 声明无需认证(即使有全局安全要求)
app.get('/public', {
  docs: {
    security: [],
  },
}, handler);

响应定义

interface ResponseConfig {
  description?: string;
  schema?: Record<string, unknown> | string;
  contentType?: string;
  example?: unknown;
  examples?: Record<string, {
    summary?: string;
    description?: string;
    value: unknown;
  }>;
  headers?: Record<string, {
    description?: string;
    schema?: { type: string };
  }>;
}

多示例响应

docs: {
  responses: {
    200: {
      description: '查询成功',
      examples: {
        admin: {
          summary: '管理员用户',
          value: { id: '1', name: 'Admin', role: 'admin' },
        },
        normal: {
          summary: '普通用户',
          value: { id: '2', name: 'User', role: 'user' },
        },
      },
    },
  },
}

自定义响应头

docs: {
  responses: {
    200: {
      description: '成功',
      headers: {
        'X-RateLimit-Remaining': {
          description: '剩余请求次数',
          schema: { type: 'integer' },
        },
      },
    },
  },
}

override

路由级配置覆盖,覆盖 src/config/default.ts 中的全局配置。

app.post('/upload', {
  override: {
    maxBodySize: '50mb',          // 覆盖全局 body 大小限制
    rateLimit: { max: 5, window: 60 },  // 收紧限流
    timeout: 30000,                // 超时 30 秒
  },
}, handler);

app.get('/public/data', {
  override: {
    rateLimit: false,  // 完全禁用限流
    cors: {
      origins: ['*'],
      credentials: false,
    },
  },
}, handler);
字段类型说明
rateLimitobject | false路由级限流配置,false 禁用
timeoutnumber请求超时(毫秒)
maxBodySizestring | number最大请求体大小
corsVextCorsConfig路由级 CORS 配置

RouteDefinition

defineRoutes() 返回的路由定义对象(内部数据结构,通常不需要直接操作)。

interface RouteDefinition {
  readonly routes: RouteRecord[];
  sourceFile: string;
  register(
    adapter: VextAdapter,
    prefix: string,
    middlewareDefs: Map<string, VextMiddleware>,
    globalMiddlewares: VextMiddleware[],
  ): void;
}
字段类型说明
routesRouteRecord[]收集到的路由记录列表
sourceFilestring来源文件路径(由 router-loader 注入)
register()Function将路由注册到底层适配器

RouteRecord

单条路由的内部数据结构:

interface RouteRecord {
  method: string;        // HTTP 方法(大写)
  path: string;          // 相对子路径
  options: RouteOptions;  // 路由配置
  handler: VextHandler;   // 路由处理函数
}

VextHandler

路由处理函数的类型定义:

type VextHandler = (
  req: VextRequest,
  res: VextResponse,
) => Promise<void> | void;

Handler 是中间件链的最后一环,不调用 next()

基本示例

const handler: VextHandler = async (req, res) => {
  const users = await app.services.user.findAll();
  res.json(users);
};

访问 App 能力

defineRoutes 的 factory 回调中,通过闭包访问 app

export default defineRoutes((app) => {
  app.get('/users/:id', async (req, res) => {
    const { id } = req.params;
    const user = await app.services.user.findById(id);

    if (!user) {
      app.throw(404, '用户不存在');
    }

    app.logger.info({ userId: id }, '查询用户成功');
    res.json(user);
  });
});

多路由注册

一个路由文件中可以注册多条路由:

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

export default defineRoutes((app) => {
  // GET /users/list
  app.get('/list', {
    validate: {
      query: { page: 'number:1-', limit: 'number:1-100' },
    },
    docs: { summary: '用户列表' },
  }, async (req, res) => {
    const { page, limit } = req.valid('query');
    const result = await app.services.user.findAll({ page, limit });
    res.json(result);
  });

  // GET /users/:id
  app.get('/:id', {
    validate: {
      param: { id: 'string:1-' },
    },
    docs: { summary: '获取用户详情' },
  }, async (req, res) => {
    const { id } = req.valid('param');
    const user = await app.services.user.findById(id);
    if (!user) app.throw(404, '用户不存在');
    res.json(user);
  });

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

  // PUT /users/:id
  app.put('/:id', {
    validate: {
      param: { id: 'string:1-' },
      body: { name: 'string:1-50?', email: 'email?' },
    },
    middlewares: ['auth'],
    docs: { summary: '更新用户' },
  }, async (req, res) => {
    const { id } = req.valid('param');
    const data = req.valid('body');
    const user = await app.services.user.update(id, data);
    res.json(user);
  });

  // DELETE /users/:id
  app.delete('/:id', {
    validate: {
      param: { id: 'string:1-' },
    },
    middlewares: ['auth'],
    docs: { summary: '删除用户' },
  }, async (req, res) => {
    const { id } = req.valid('param');
    await app.services.user.delete(id);
    res.status(204).json(null);
  });
});

注意事项

不要直接在 app 上调用 HTTP 方法

defineRoutes 返回的 app 是一个收集器,不是真正的应用实例。直接在应用实例上调用 HTTP 方法会抛出错误:

// ❌ 错误用法
import { createApp } from 'vextjs';
const { app } = createApp(config);
app.get('/hello', handler); // 抛出错误!

// ✅ 正确用法
import { defineRoutes } from 'vextjs';
export default defineRoutes((app) => {
  app.get('/hello', handler); // OK
});

路由文件必须 default export

// ✅ 正确
export default defineRoutes((app) => { ... });

// ❌ 错误 — router-loader 无法识别
export const routes = defineRoutes((app) => { ... });

路由路径规范化

框架自动处理以下路径边界情况:

前缀子路径最终路径
/users/list/users/list
/users//users
/users/:id/users/:id
///
//health/health
/api/users``/api/users