使用 Swagger 构建 Express API Server 的文档系统

上一篇博客所说,好的文档系统对 API Server 至关重要,本文介绍在 Express 框架中使用 Swagger 构建一个良好的项目文档系统的基本流程,同时明确一些实践过程中肯定会遇到的问题的解决方案。本文遵循Swagger 2.0使用规范。

图片显示错误

目标

  • 文档生成的「源」(或者说「依据」)与代码不分离,即直接用jsdoc注释生成文档;
  • 可以用同样的「源」同时实现对接口输入输出参数的验证,最大化保证文档与后端具体实现之间的一致性;
  • 文档在线可用性测试,并且可以完美解决跨域请求的问题;
  • 在后端接口还未完成时,可以 Mock 返回数据;
  • 最好能自动生成一些测试数据甚至自动进行测试;

从 JSDoc 到可视化文档

Step 1:定义接口模型

在 Controller 层每一条路由的函数注释上(具体来说,Routes 目录下或 Controller 目录下均可,只要配置好Step 2中的swagger-jsdoc,明确「源」所在的目录即可)按Swagger YAML语法定义接口模型,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* @swagger
* definition:
* Puppy:
* properties:
* name:
* type: string
* breed:
* type: string
* age:
* type: integer
* sex:
* type: string
*/

/**
* @swagger
* /api/puppies:
* get:
* tags:
* - Puppies
* description: Returns all puppies
* produces:
* - application/json
* responses:
* 200:
* description: An array of puppies
* schema:
* $ref: '#/definitions/Puppy'
*/
router.get('/api/puppies', db.getAllPuppies);

/**
* @swagger
* /api/puppies:
* post:
* tags:
* - Puppies
* description: Creates a new puppy
* produces:
* - application/json
* parameters:
* - name: puppy
* description: Puppy object
* in: body
* required: true
* schema:
* $ref: '#/definitions/Puppy'
* responses:
* 200:
* description: Successfully created
*/
router.post('/api/puppies', db.createPuppy);

一些抽象出来的definition直接在前面的/** */注释中定义即可。

在生成的完整配置中,同一个路径下的配置(比如上述示例中/api/puppies下的getpost)会合并在这同一个路径之下,所以同一个路径下的全局配置只用写一遍就行了(比如下文Mock返回数据小节中使用Swagger Router中间件需要的x-swagger-router-controller配置)。

Step 2:生成 Swagger 接口定义

swagger-jsdoc 生成 JSON 格式的 Swagger 接口定义。
Demo:mjhea0/node-swagger-api

这里不用把生成的 JSON 保存在本地磁盘上,直接用一个变量引用即可。

Step 3:用 Swagger UI 生成可视化文档

在线查看:打开http://petstore.swagger.io/,在顶部的 URL 栏输入可以获取Step 2中生成的 JSON 格式的 Swagger 文档定义的 URL,一般来说可以是http://localhost:3000/swagger.json (需要自己手动在代码中书写路由和请求的返回) 在使用了后文要说明的Swagger UI 中间件之后按照默认配置是 http://localhost:3000/api-docs (注意最后没有/)。这种方式需要解决跨域请求的问题,详见后文。

本地离线查看:直接在本项目 public(静态文件目录)下放置离线版Swagger UI,直接打开即可查看。详见Step 2中的 Demo 及作者的博文说明。使用swagger-tools中的Swagger UI 中间件,如果你直接使用默认配置:

这个中间件可以让你在开发时再也无需操心可视化文档的前端实现和如何查看自动生成 Swagger 接口定义(以便确定是否符合规范和自己的需求)的问题。

一些常见问题

解决跨域请求的问题

官方说明见:https://github.com/swagger-api/swagger-ui#cors-support
app.use('/', routes);之前加一个如下的中间件设置一些 cors 相关的头可以解决问题:

1
2
3
4
5
6
7
8
9
10
11
12
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', 'http://petstore.swagger.io');
res.setHeader(
'Access-Control-Allow-Methods',
'GET, POST, DELETE, PUT, PATCH, OPTIONS'
);
res.setHeader(
'Access-Control-Allow-Headers',
'Content-Type, api_key, Authorization'
);
next();
});

接口参数验证

使用swagger-tools中的Swagger MetadataSwagger Validator中间件:

其中 Swagger Metadata 中间件做了匹配请求路由与 Swagger 定义路由以及解析参数的工作,Swagger Validator 中间件做了验证参数类型和其他已定义的参数限制的工作。

Mock 返回数据

使用swagger-tools中的Swagger Router 中间件即可实现。

正常情况下,根据你的 Swagger 定义会返回Response Code200(当 Swagger 定义中已定义200的返回时)的类似这样的数据:

1
2
3
4
5
6
7
8
[
{
"name": "Sample text",
"breed": "Sample text",
"age": 1,
"sex": "Sample text"
}
]

自动生成测试代码

使用apigee-127/swagger-test-templates可以根据你的 Swagger 定义自动生成对所有接口功能测试的脚手架代码(基本可以自动确定的地方都自动生成了),在你把自动生成的代码写入磁盘文件后,只需修改极少量的地方(一般是提供一些需要测试的参数)就可以使用测试。

自动生成的代码长成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
'use strict';
var chai = require('chai');
var ZSchema = require('z-schema');
var validator = new ZSchema({});
var supertest = require('supertest');
var api = supertest('http://localhost:3000'); // supertest init;

chai.should();

describe('/api/puppies', function() {
describe('get', function() {
it('should respond with 200 An array of puppies', function(done) {
/*eslint-disable*/
var schema = {
type: 'array',
items: {
$ref: '#/definitions/Puppy'
}
};

/*eslint-enable*/
api
.get('/api/puppies')
.set('Accept', 'application/json')
.expect(200)
.end(function(err, res) {
if (err) return done(err);

validator.validate(res.body, schema).should.be.true;
done();
});
});
});

describe('post', function() {
it('should respond with 200 Successfully created', function(done) {
api
.post('/api/puppies')
.set('Accept', 'application/json')
.send({
puppy: 'DATA GOES HERE'
})
.expect(200)
.end(function(err, res) {
if (err) return done(err);

res.body.should.equal(null); // non-json response or no schema
done();
});
});
});
});

之后可以考虑用 gulp 把测试串联起来进行自动化测试。npm 上已经有这样的包,不过我还没有试过。

目前这个包对于Swagger 2.0的支持还不是很完全,尤其是对$ref不能自动解析,这样的话需要手动改动的测试代码多一些,不过他们正在着力解决这个问题,估计下一个版本(1.3.0)就会加入对$ref的解析,详细的讨论可以看这个 issue。同时,他们还在考虑添加通过 JSON-Schema 自动批量生成测试数据的功能,目测也将在下一个版本中推出,值得期待。

Mock 或 Swagger UI 失效

由于异步的问题,如果你把app.lieten写在swaggerTools.initializeMiddleware的回调函数外面,那很可能在你的应用已经启动时,swagger-tools的中间件并没有加载完毕,导致中间件失效(不会报错)。

鉴于此,应该尽量把swaggerTools.initializeMiddleware写在中间件链的后面部分,然后把位于其后的app.use(比如app.use('/', routes);)和app.listen写在swaggerTools.initializeMiddleware的回调函数内部。所以,Express 4中提倡的用./bin/www来启动应用的要求在这里可能无法被遵循了。

更详细的讨论看这个 issue

另外,Mock 失效还有可能是你已经提供了对应 Controller 来 Handle 对应的 Route 请求。并不是将 Swagger Router 中间件中的useStubs设为true就一定会启动 Mock,官方对此说明是:

Stubs only work for requests where the controller and/or controller method is missing. Since you have a working controller method, enabling stub mode doesn’t do anything. It’s working as designed.

更详细的讨论可以看这个 issue

调试技巧

使用DEBUG=swagger-tools* node app启动项目,控制台会输出更多详细的信息。

完整的 Demo

Maples7/swagger-express-demo: 将前面所讲的内容整合进了一个小示例中,以供参考。

上手必读

  1. swagger-spec
  2. swagger-tools/docs/QuickStart.md
  3. swagger-tools/docs/Middleware.md
  4. swagger-tools/docs/API.md