In this article i’m going to show you how to take the pain out of documenting your Node API using Swagger, and take it a step further by streamlining the process and making it more maintainable using doctopus. We’ll use a simple Node express app and the swagger-ui interface. While I’ll touch a bit on Swagger, I will mostly be focusing on generating documentation within the app rather than the features of the swagger itself.
Swagger is a popular open source documentation framework with a ton of momentum and plugins, tools, and docs available out of the box. It has a declarative syntax and can be generated using most programming languages. From it’s exploding popularity, the swagger specification has been renamed to the OpenAPI Specification and is now the de facto standard for defining schema definitions.
The Swagger team provides the Swagger UI for free which renders a nice web interface from the JSON you generate that can be used to browse API documentation. One of the really nice features is that it allows you to make requests to your endpoints directly from the UI, which helps immensely during testing / debugging. The easy to use interface makes it easy to fill in path, query, or JSON parameters and provides bumpers to prevent mistakes, and provides a significantly better UX than Postman.
Here are examples of a simple swagger spec written in both YAML / JSON
# this is an example a YAML Swagger Spec
swagger: '2.0'
info:
title: Uber API
description: Move your app forward with the Uber API
version: '1.0.0'
basePath: /v1
produces:
- application/json
paths:
/products:
get:
summary: Product Types
description: |
Blah blah blah
parameters:
- name: id
in: query
description: Product Id.
required: false
type: number
format: double
tags:
- Products
responses:
200:
description: An array of products
schema:
type: array
items:
$ref: '#/definitions/Product'
definitions:
Product:
type: object
properties:
product_id:
type: string
description: Unique identifier.
// this is an example a JSON Swagger Spec
{
"swagger": "2.0",
"info": {
"title": "Uber API",
"description": "Move your app forward with the Uber API",
"version": "1.0.0"
},
"basePath": "/v1",
"produces": ["application/json"],
"paths": {
"/products": {
"get": {
"summary": "Product Types",
"description": "Blah blah blah",
"tags": ["Products"]
}
}
}
}
// remaining code ommitted for brevity
Like all production applications, we have many API endpoints exposed within both our internal and external facing web services. After writing up a bunch of docs we found ourselves re-writing artifacts over and over and the maintenance burden was real. For a simple schema change, docs had to be updated everywhere! We decided to take an engineering approach.
The first a problem was that writing out swagger as JSON in separate files gave updating the docs an out of sight and out of mind feeling. When you’d update a route handler with a new query argument, sometimes you’d forget to update the docs — so things drift apart very quickly. Missing parameters in the swagger docs mean incorrect documentation which means API consumers call your API incorrectly. No good.
Secondly, the folder structure was burdensome to work with. Whenever changes were made within services or moved folders around, you’d have to make the corresponding changes in a foreign docs directory which didn’t always happen. Over time they become out of sync, and so less and less relevant. Lastly, we were suffering from schema overload. Between joi for object validation, mongoose the object document mapper, avro for messaging, typescript interfaces, and swagger — we’d have an awful lot of schema definitions to update for every model change. We were looking for some type of solution which would allow alleviate some of the redundancy.
We tried using JSDoc which allowed you to add docs inline comments within code (basically YAML) but it was painful — especially when there was redundant data which is inevitable in a large application. As if YAML wasn’t bad enough, now you need to write it within JavaScript comments. Yuck. Our Solution
Finally, we decided to take a more dynamic approach. We build a light weight library called doctopus which exposes a fluent api for declaring documentation in code near route declarations, which compiles into swagger at build time.
The result is an easy to use api enabling re-use of parameter and schema definitions, colocated with corresponding code. The colocation is everything. When the documentation declaration is right next to the implementation, developers maintain it. It becomes part of the development process similar to colocating tests.
Here’s an example using the fluent API.
const app = require('express')();
const doctopus = require('doctopus');
const docs = new doctopus.DocBuilder();
const docFactory = new doctopus.Doc();
docs.set('title', 'My Express App');
docs.add(
'/swagger',
docFactory
.get()
.group('Documentation')
.description('Gets a Swagger Specification')
.summary('Swagger')
.onSuccess(200, {
description: 'Swagger Spec',
schema: Doc.object(),
})
.build(),
);
app.get('/swagger', (req, res) => res.send(docs.build()));
app.listen('3000');
This idea of colocation has worked so well for us, that we’ve decided to take it to the next level. We enhanced the doctopus API to optionally expose a decorator API.
Using decorators, docs are declared right alongside controller methods improving accuracy and maintainability. Since all methods in the controller are documented, another bonus benefit is that when additional methods are added, developers usually maintain the status quo of documentation, resulting in improved coverage.
Here’s an example of the new decorator API usage.
import { Doc, DocBuilder, get, group, param, response, route } from 'doctopus';
// set default for all controller methods
@group('Cats')
class CatCtrl {
// http get request
@get
// set route
@route('/cats/{id}')
// override group of a specific method
@group('Orders')
public findOne(req, res) {
res.send({});
}
@get
@route('/cats')
// add a param
@param({
in: 'query',
type: 'string',
name: 'name',
})
// declare response
@response({
description: 'All Cats',
/*
[{ name: 'foofoo' }]
*/
schema: Doc.arrayOf(
Doc.inlineObj({
name: Doc.string(),
}),
), // schema, see schema api
})
public findAll(req, res) {
res.send([]);
}
}
const docs = new DocBuilder();
// docBuilder instance will read the docs
docs.use(CatCtrl);
Best of all, since all of the docs are written in code you get code completion and compile time type safety!
If you’d like to try out these examples feel free to clone the repository, and try out the examples locally.
I hope these open source contributions help improve your documentation workflow.
Thank you for reading! To be the first notified when I publish a new article, sign up for my mailing list!