Automatically Generate Swagger JSDoc for Express APIs

Torstein Frogner Knutson
Fremtind
Published in
7 min readMay 27, 2024
This used to be you. Now you can just press a button and join the poolparty instead.

Ever wished there was a button that just wrote the documentation for you?

1. Introduction
- The Challenge: Manual Documentation
- The Solution: Automatic Swagger JSDoc Generation
2. The Python Script
- Overview of SwaggerGen.py
- Input and Output
- Keeping Code Base Clean
- Generating Separate Swagger JSDoc Schema and Companion Document
- Swagger Companion
3. Setting up Swagger Docs in Your App
4. Running the Script
- Execution of SwaggerGen.py
- Outputs of the Script
- Debugging and Tweaks
5. Examples:
- Example of Router (usersRouter.js)
- Example of Controller (usersController.js)
- Example of generated output
- Running SwaggerGen.py with the Example
6. Conclusion

The Challenge: Manual Documentation

“Swagger JSDoc simplifies the process of documenting your Express API, improves collaboration among team members, and enhances the overall quality of your API.”

We are currently translating many of our services written in RPGLE using PCML, to REST using JSON. For all our new endpoints in our Node Express app, we need to make OpenAPI Swagger documentation.

However, writing out detailed API documentation can be a bit tedious, especially when you’re juggling multiple endpoints, HTTP methods (GET, POST, DELETE, PUT), and controller logic. Manually crafting Swagger annotations for each route can be time-consuming and error-prone.

The Solution: Automatic Swagger JSDoc Generation

Instead of hand-coding every detail, we’ll leverage a Python script that reads your Express router and controller files. This script intelligently extracts relevant information and automatically generates Swagger JSDoc for you. Let’s break it down:

  1. The Python Script
  • The Python script is called SwaggerGen.py
  • It takes your express router and controller files as input.
  • The script analyzes various parts of these files, such as route paths, parameters, and responses.
  • The script outputs Swagger JSDoc you can use with OpenAPI.
  • The code can be found here on Github. Feel free to contribute!

2. Clean Routers and Controllers

  • You can insert JSDoc comments directly into your router and controller code and generate Swagger docs from that. However, we prefer to keep our code base clean and free from Swagger-specific annotations.
  • Our approach: Generate a separate Swagger JSDoc schema and a companion document (let’s call it “Swagger Companion”).

3. Swagger Companion

  • The Swagger Companion is a Swagger JSDoc-formatted .js document.
  • It includes detailed descriptions of your API endpoints, request parameters, response formats, and more.
  • You can keep the generated swagger docs anywhere in your app by setting this code inapp.js.
const specs = swaggerJsDoc(options);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs, options));

4. Running the Script

Execute the SwaggerGen.py script inside the same directory as your router and controller files. It produces four outputs in this sequence:

  • Swagger Companion .json
  • Swagger Companion JSDoc .js
  • Swagger Schema .json
  • Swagger Schema JSDoc.js

4b. Debugging and tweaks

  • The .json files can be used for debugging and can be deleted. It is easier to spot possible autogenerated mistakes in the syntax when reading a .json file using a linter, before it becomes JSDoc syntax during development.
  • Test it out with your own routers and controllers. You will see the output written to the new files instantly.
  • The python code is straight forward — try to add or mute some things to better fit your style.

Example: Simple Router and Controller . js

Let’s consider a basic example. Suppose we have an Express app with just a single endpoint for retrieving all users. Here is the router and the controller:

  1. Router (usersRouter.js):
// ***********************************************************
// Router to get all users
// ***********************************************************
router.get('/getall',
check('userid', 'userid field not filled in correctly')
.trim()
.not()
.isEmpty(),
check('clientid', 'clientid field not filled in correctly')
.trim()
.not()
.isEmpty(),
async (req, res) => {
// Check for errors and return error code with cause
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({errors: errors.array()});
}

const d = new Date();
let startTs = d.toISOString();
var responseData;

// Routing request to controller
try {
responseData = await userController.getAllUsers(req);
result = JSON.parse(responseData);
res.json(result);
} catch (e) {
return res.status(500).json({error: e.message});
}

}
);

2. Controller (usersController.js):

// ***********************************************************
// Controller to get all users
// ***********************************************************
async function getAllUsers (req, res) {
let result = {};
let sqlresult = {};
let userprofiles = [];
let starttid = Date.now();
let metaUt = new Meta.MetaUt(req.get("messageId"), req.get("userid"),'REST_API', );
let sqldata = 'select a,b' +
' from table2' +
' where z2user = usergroup_id1';

console.log(sqldata);

try {
sqlresult = await db2.executeStatement(sqldata, null, req.get("messageId"));
} catch (e) {
result = e.message;
return result;
}

let i = 0;
sqlresult.forEach(element => {

let userprofile = {
"id_number": element.Z7PERS,
"name": element.Z2NAMN.trim()
}

i = i + 1;

userprofileer.push(userprofile);
});

if (i > 0) {
metaUt.resultCode = '00000';
metaUt.resultText = 'OK';
} else {
metaUt.resultCode = '00001';
metaUt.resultText = 'User not found';
}

metaUt.resultTime = (Date.now() - starttid).toString();

result.meta = metaUt;
result.nicebrukerut = userprofileer;

return JSON.stringify(result);
}

The usersRouter.jsand usersController.jsis read in as string inputs in SwaggerGen.py

3. SwaggerGen(SwaggerGen.py) :

#please get the latest maintained code from the repo on Github 

from dataclasses import dataclass
import re
import os
import json
from swaggerSChemaGen_tester import generateswagger

@dataclass
class Route:
path: str
method: str
description: str
responses: dict

def get_service_name_directory(service_name):
current_dir = os.path.dirname(os.path.realpath(__file__))
project_root = os.path.dirname(current_dir)
service_name_directory = os.path.join(current_dir, service_name)
service_name_directory = service_name_directory.replace(project_root, '')
service_name_directory = service_name_directory.lstrip('/\\')
service_name_directory = service_name_directory.replace('\\', '/')
return service_name_directory.rstrip('/')

def get_route_info(text):
matches = re.findall(r'router\.(get|post|put|delete|patch)\(.*?\'(.*?)\'', text, re.DOTALL)
swagger_doc = []
for match in matches:
method, route_path = match
parameters = re.findall(r'check\(\'(.*?)\'', text)
swagger_parameters = [generate_swagger_parameter(name) for name in parameters]
route_obj = Route(route_path, method, service_description, response_codes)
swagger_comment = generate_swagger_comment(route_obj, swagger_parameters)
route_path = f"/{service_name_directory}{route_obj.path}"
method = route_obj.method
swagger_doc.append({route_path: {method: swagger_comment[method]}})
return swagger_doc

def write_to_file(swagger_doc, file_name):
swagger_doc_dict = {}
for doc in swagger_doc:
for key, value in doc.items():
if key in swagger_doc_dict:
swagger_doc_dict[key].update(value)
else:
swagger_doc_dict[key] = value
with open(file_name, 'w') as file:
json.dump(swagger_doc_dict, file, indent=2)

def generate_swagger_parameter(name):
# Check if the parameter is one of the predefined parameters
if name in ['clientid', 'userid', 'messageid']:
# Return a reference to the predefined parameter in Swagger components
return {
"$ref": f"\'#/components/parameters/{name}\'"
}
else:
# Return a reference to the parameter in Swagger components
return {
"name": name,
"in": "query",
"required": True,
"description": f"Description for {name}", # Update this to the correct description
"schema": {
"type": "string"
},
"example": "Update this for a real example"
}

def generate_swagger_comment(route: Route, parameters) -> dict:
comment = {
route.method: {
"tags": [service_name],
"summary": route.description,
"parameters": parameters,
"responses": {}
}
}
for code, desc in route.responses.items():
if code == 200:
response = {
"description": desc,
"content": {
"application/json": {
"schema": {
"$ref": f"\'#/components/schemas/{service_name}\'"
}
}
}
}
else:
response = {
"description": desc
}
comment[route.method]["responses"][str(code)] = response
return comment

def adjust_formatting(swagger_doc_str):
swagger_doc_str = swagger_doc_str[1:]
swagger_doc_str = '\n'.join('* ' + line for line in swagger_doc_str.split('\n'))
swagger_doc_parts = swagger_doc_str.split(f'"/{service_name_directory}')
swagger_doc_str = f'*/\n\n/**\n* @swagger\n* "/{service_name_directory}/'.join(swagger_doc_parts[1:])
swagger_doc_str = '/**\n* @swagger\n* "/' + service_name_directory + '/' + swagger_doc_parts[0] + swagger_doc_str
swagger_doc_str = swagger_doc_str.rstrip(',* ')
swagger_doc_str += '\n */' #hooray, stars!
return swagger_doc_str

def adjust_swagger_comments(swagger_comments):
swagger_comments = re.sub(r'\n \* ', '\n* ', swagger_comments)
swagger_comments = re.sub(r'\* \n\* /', '\n /', swagger_comments)
swagger_comments = re.sub(r'\n /":', '\n":', swagger_comments)
swagger_comments = re.sub(r'\/\n \* \{', '/ {', swagger_comments)
swagger_comments = re.sub(r'\* \*/', '*/', swagger_comments)
swagger_comments = re.sub(r'^": \{$', '* ": {', swagger_comments, flags=re.MULTILINE)
swagger_comments = re.sub(r'^ \*', '*', swagger_comments, flags=re.MULTILINE)
swagger_comments = re.sub('//', '/', swagger_comments)
swagger_comments = re.sub(r',\n\* {\n\* \*/\n\n', '*/\n', swagger_comments)
swagger_comments = re.sub(r'\* }\*/', '* }\n*/', swagger_comments)
pattern = r'(@swagger\n\* "/)(.*?)(/\* \n\* {\n\* /")'
replacement = r'\1\2"'
swagger_comments = re.sub(pattern, replacement, swagger_comments)
swagger_comments = re.sub(r'\]\s*$', '}', swagger_comments, flags=re.MULTILINE)
return swagger_comments


# Set parameters
service_name = "users"
controller_name = 'usersController.js'
router_name = 'usersRouter.js'
service_description = f"{service_name} routes - get all users."
response_codes = {200: "OK", 400: "Bad request", 401: "Unauthorized", 404: "Not found",}

service_name_directory = get_service_name_directory(service_name)

# Clear the output file at the start
open(f'{service_name}Swagger_generated.json', 'w').close()

# Read the file
with open(f'{service_name}.js', 'r') as file:
lines = file.readlines()

# Combine the lines into a single string
text = ''.join(lines)

swagger_doc = get_route_info(text)
write_to_file(swagger_doc, f'{service_name}Swagger_generated.json')

# Read the JSON Swagger document
with open(f'{service_name}Swagger_generated.json', 'r') as file:
swagger_doc = json.load(file)

# Convert the JSON Swagger document to a string with indentation
swagger_doc_str = json.dumps(swagger_doc, indent=2)

swagger_doc_str = adjust_formatting(swagger_doc_str)

# Write the JSDoc Swagger document to a file
with open(f'{service_name}Swagger_generated.js', 'w') as file:
file.write(swagger_doc_str)

# Read the Swagger comments into a string
with open(f'{service_name}Swagger_generated.js', 'r') as file:
swagger_comments = file.read()

swagger_comments = adjust_swagger_comments(swagger_comments)

# Write the adjusted Swagger comments back to the file
with open(f'{service_name}Swagger_generated.js', 'w') as file:
file.write(swagger_comments)

# Generate the schema (get this code in the github repo)
generateswaggerSchema(service_name, controller_name, router_name)

4. Generated output — Swagger JSDoc

The output when reading in a router and controller.
The result: Swagger JSDoc filled with stars * * *

/**
* @swagger
* "/swaggerFolder/users_router_example/
/getall": {
* "get": {
* "tags": [
* "users_router_example"
* ],
* "summary": "users_router_example service description here",
* "parameters": [
* {
* "$ref": "'#/components/parameters/userid'"
* },
* {
* "$ref": "'#/components/parameters/clientid'"
* },
* {
* "$ref": "'#/components/parameters/userid'"
* },
* {
* "$ref": "'#/components/parameters/messageid'"
* }
* ],
* "responses": {
* "200": {
* "description": "OK",
* "content": {
* "application/json": {
* "schema": {
* "$ref": "'#/components/schemas/users_router_example'"
* }
* }
* }
* },
* "400": {
* "description": "Bad request"
* },
* "401": {
* "description": "Unauthorized"
* },
* "404": {
* "description": "Not found"
* }
* }
* }
* },
*/

Link to code on Github

You can find all the files you need here. We invite you to consider contributing to the code. Lets free ourselves from the tyranny of manual documentation.

Conclusion

In this article, we’ve covered an automatic approach for generating Swagger JSDoc from Express controllers and routers.

--

--