KOA技术分享

专注 Koa.js 框架的编程知识分享

Koa.js API 文档自动生成与 OpenAPI 实践

引言

良好的 API 文档是前后端协作的基础。本文介绍如何在 Koa.js 项目中实现 API 文档的自动生成,基于 OpenAPI 3.0 规范,支持 Swagger UI 可视化展示和代码生成。

技术选型

工具 功能 特点
koa-swagger-decorator 注解方式自动生成 装饰器风格,代码简洁
swagger-jsdoc JSDoc 注释解析 无需装饰器,兼容性好
@koa/swagger 中间件方式集成 运行时生成,动态更新

环境安装

# 安装依赖
npm install koa-swagger-decorator swagger-ui-koa

# 或使用组合方案
npm install koa2-swagger-ui swagger-jsdoc

decorators 方案实现

使用装饰器方式定义 API 文档:

// swagger.js - Swagger 配置
import { setMeta, swagger, updateOpenApi } from 'koa-swagger-decorator';

export const swaggerOptions = {
  title: 'Koa.js REST API',
  version: '1.0.0',
  description: '企业信息化系统 API 文档',
  definition: {
    openapi: '3.0.0',
    info: {
      title: '企业信息化系统 API',
      version: '1.0.0',
      description: 'EIMS 系统 RESTful API 接口文档',
      contact: {
        name: 'API Support',
        email: 'webnic@qq.com'
      }
    },
    servers: [
      {
        url: 'http://localhost:3000',
        description: '开发环境'
      },
      {
        url: 'https://api.eims.com',
        description: '生产环境'
      }
    ],
    components: {
      securitySchemes: {
        bearerAuth: {
          type: 'http',
          scheme: 'bearer',
          bearerFormat: 'JWT'
        }
      }
    },
    security: [{ bearerAuth: [] }]
  }
};

// 在应用启动时初始化
export function initSwagger(router) {
  swagger(swaggerOptions)(router);
  return router;
}
// user.controller.js - 用户接口
import {
  controller,
  Get,
  Post,
  Put,
  Delete,
  Params,
  Query,
  Body,
  Summary,
  Description,
  Tags,
  Middleware,
  Responses
} from 'koa-swagger-decorator';
import { validateBody, validateParams } from '../middleware/validation';
import { authenticate } from '../middleware/auth';

// 定义请求/响应 DTO
const UserDTO = {
  id: { type: 'number', required: false },
  username: { type: 'string', required: true, example: 'admin' },
  email: { type: 'string', required: true, example: 'admin@example.com' },
  role: { type: 'string', enum: ['admin', 'user', 'guest'], default: 'user' },
  createdAt: { type: 'string', format: 'date-time' }
};

const CreateUserSchema = {
  username: { type: 'string', minLength: 3, maxLength: 50 },
  email: { type: 'string', format: 'email' },
  password: { type: 'string', minLength: 6 }
};

@Tags('用户管理')
@Middleware(authenticate)
@controller('/api/users')
export class UserController {

  @Summary('获取用户列表')
  @Description('获取所有用户,支持分页和筛选')
  @Get('/')
  @Responses(200, '返回用户列表')
  @Responses(401, '未授权')
  async getUsers(ctx) {
    const { page = 1, pageSize = 10, role } = ctx.query;

    const users = await UserService.findAll({ page, pageSize, role });
    const total = await UserService.count({ role });

    ctx.body = {
      code: 0,
      data: {
        list: users,
        pagination: { page, pageSize, total }
      }
    };
  }

  @Summary('获取用户详情')
  @Description('根据用户ID获取详细信息')
  @Get('/:id')
  @Responses(200, '返回用户详情')
  @Responses(404, '用户不存在')
  async getUserById(@Params('id') id: number) {
    const user = await UserService.findById(id);
    if (!user) {
      ctx.throw(404, 'User not found');
    }
    ctx.body = { code: 0, data: user };
  }

  @Summary('创建用户')
  @Description('创建新用户,需要管理员权限')
  @Post('/')
  @Responses(201, '创建成功')
  @Responses(400, '参数错误')
  async createUser(@Body() body: typeof CreateUserSchema) {
    // 验证参数
    await validateBody(CreateUserSchema)(ctx);

    const user = await UserService.create(body);
    ctx.status = 201;
    ctx.body = { code: 0, data: user };
  }

  @Summary('更新用户')
  @Description('更新用户信息')
  @Put('/:id')
  @Responses(200, '更新成功')
  @Responses(403, '无权限')
  async updateUser(@Params('id') id: number, @Body() body: Partial) {
    const user = await UserService.update(id, body);
    ctx.body = { code: 0, data: user };
  }

  @Summary('删除用户')
  @Description('删除指定用户(软删除)')
  @Delete('/:id')
  @Responses(204, '删除成功')
  async deleteUser(@Params('id') id: number) {
    await UserService.softDelete(id);
    ctx.status = 204;
  }

  @Summary('修改密码')
  @Description('用户修改自己的密码')
  @Post('/:id/password')
  @Responses(200, '修改成功')
  @Responses(400, '密码错误')
  async changePassword(
    @Params('id') id: number,
    @Body() body: { oldPassword: string; newPassword: string }
  ) {
    const { oldPassword, newPassword } = body;

    // 验证旧密码
    const valid = await UserService.verifyPassword(id, oldPassword);
    if (!valid) {
      ctx.throw(400, 'Old password is incorrect');
    }

    await UserService.updatePassword(id, newPassword);
    ctx.body = { code: 0, message: 'Password updated successfully' };
  }
}

路由注册与 Swagger UI

// app.js - 应用入口
import Koa from 'koa';
import Router from 'koa-router';
import {mount} from 'koa-mount';
import {swaggerKoa} from 'koa-swagger-decorator';
import {initSwagger} from './swagger';

const app = new Koa();
const router = new Router();

// 初始化 Swagger
initSwagger(router);

// 挂载 Swagger UI
router.get('/api-docs', swaggerKoa());

// 注册路由
import {UserController} from './controllers/user.controller';
router.applyRoutes(UserController);

// 中间件顺序很重要
app.use(router.routes());
app.use(router.allowedMethods());

// 静态文件(Swagger UI)
app.use(mount('/swagger', KoaStatic(path.join(__dirname, '..', 'node_modules', 'swagger-ui-dist'))));

console.log('Swagger UI: http://localhost:3000/api-docs');

JSDoc 方案实现

对于不想使用装饰器的项目,可以使用 JSDoc 注释:

// user.service.js - 使用 JSDoc 注释
const router = require('koa-router')();

/**
 * @openapi
 * /api/users:
 *   get:
 *     summary: 获取用户列表
 *     tags:
 *       - 用户管理
 *     parameters:
 *       - name: page
 *         in: query
 *         schema:
 *           type: integer
 *           default: 1
 *       - name: pageSize
 *         in: query
 *         schema:
 *           type: integer
 *           default: 10
 *     responses:
 *       200:
 *         description: 成功
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 code:
 *                   type: integer
 *                 data:
 *                   type: object
 */
router.get('/api/users', async ctx => {
  // ...
});

module.exports = router;

OpenAPI 规范扩展

自定义 OpenAPI 组件定义可复用的 schema:

// schema.js - 通用 Schema 定义
export const schemas = {
  // 错误响应
  ErrorResponse: {
    type: 'object',
    properties: {
      code: { type: 'integer', example: 400 },
      message: { type: 'string', example: '请求参数错误' },
      details: { type: 'object' }
    }
  },

  // 分页响应
  PaginatedResponse: {
    type: 'object',
    properties: {
      list: {
        type: 'array',
        items: { type: 'object' }
      },
      pagination: {
        type: 'object',
        properties: {
          page: { type: 'integer' },
          pageSize: { type: 'integer' },
          total: { type: 'integer' },
          totalPages: { type: 'integer' }
        }
      }
    }
  },

  // 订单模型
  Order: {
    type: 'object',
    required: ['orderNo', 'amount'],
    properties: {
      id: { type: 'integer' },
      orderNo: { type: 'string' },
      amount: { type: 'number' },
      status: { type: 'string', enum: ['pending', 'paid', 'shipped', 'completed'] },
      items: {
        type: 'array',
        items: { $ref: '#/components/schemas/OrderItem' }
      }
    }
  }
};

代码生成与测试

使用 OpenAPI 规范生成客户端代码:

# 使用 openapi-generator 生成客户端
npx @openapitools/openapi-generator-cli generate \
  -i http://localhost:3000/api-docs/swagger.json \
  -g typescript-axios \
  -o ./src/api-client

# 生成服务端 stub
npx @openapitools/openapi-generator-cli generate \
  -i http://localhost:3000/api-docs/swagger.json \
  -g koa-server \
  -o ./src/server-stub

# 生成 Postman 集合
npx @openapitools/openapi-generator-cli generate \
  -i http://localhost:3000/api-docs/swagger.json \
  -g postman \
  -o ./docs/postman.json

# 生成 Markdown 文档
npx @openapitools/openapi-generator-cli generate \
  -i http://localhost:3000/api-docs/swagger.json \
  -g markdown \
  -o ./docs

总结

Koa.js 项目中实现 API 文档自动化的核心要点:

良好的 API 文档不仅便于前后端协作,还能作为契约测试和自动化的基础。

← 下一篇:Koa.js API文档自动生成与Swagger集成