JavaScriptNode.jsAPIDocs

How to Document an Express API with Swagger UI and JSDoc

November 29, 2020

Originally published on DEV ↗

JSDoc is a popular tool for generating documentation from comments in the source code of your app. This serves two purposes. First, the documentation is directly available to anyone viewing the source code. Second, the comments can be compiled later into a complete set of reference documentation.

Swagger provides a tool for presenting this documentation: Swagger UI. Swagger UI creates a web page from OpenAPI Specification definitions. As this tutorial will show, these definitions can be written in YAML directly in JSDoc comments.

In this tutorial, you will set up a Swagger UI documentation web page for an Express API. You can then write JSDoc comments in your API's source code to generate the OpenAPI definitions. By the end, you will have documentation that follows the OpenAPI Specification, presented from a custom /docs endpoint added to your Express API:

Swagger UI Docs Page

Prerequisites

To complete this tutorial, you'll need

  • Familiarity with REST APIs and Express

  • Node.js installed on your system

  • An Express-based REST API running on a local Express server. If you don't have one, you can install the Express API used in this tutorial. It retrieves user data from JSONPlaceholder.

To install and run the sample Express API, first clone the repository (replace test-api with the directory name of your choosing):

git clone https://github.com/kabartolo/jsonplaceholder-express-api test-api
Enter fullscreen mode Exit fullscreen mode

Next, run the following commands to start the Express server (replace test-api with the name of the directory you just created):

cd test-api
npm install
npm run start
Enter fullscreen mode Exit fullscreen mode

Navigate to localhost:3000 to see the API. You should see links to /users and /users/1.

API Homepage

Navigate to either of these to see user data from JSONPlaceholder.

The code added during this tutorial can be found in the repository's docs branch.

Terminology

OpenAPI is the name of the specification, while Swagger is the set of tools that implement this specification. See What Is the Difference Between Swagger and OpenAPI?

This tutorial uses the following API-related terms and definitions defined by OpenAPI:

    https://api.example.com/v1/users?role=admin&status=active
    \________________________/\____/ \______________________/
             server URL       endpoint    query parameters
                                path
Enter fullscreen mode Exit fullscreen mode
  • Server URL or base URL: The base URL for all API endpoints: localhost:3000 or example.com/api
  • Endpoint path: The path representing the location of the resource (relative to the base URL): /users or /users/1
  • Operation: The HTTP method used to manipulate endpoint paths: GET, POST, PUT, DELETE
  • Resource: Information representing a real-world object (e.g., a user or book), usually returned by the API as JSON data. Represented by a database model in Express.

The full URL used to retrieve data from the API is formed by adding the endpoint to the base URL: localhost:3000/users.

Step 1: Set up the application

1.1: Install swagger-jsdoc and swagger-ui-express

To create a Swagger UI page from JSDoc comments, you’ll need a way to pass your documentation to Swagger UI:

To install swagger-jsdoc and swagger-ui-express to your Express API, run

npm install swagger-jsdoc@5.0.1 --save-exact
npm install swagger-ui-express --save
Enter fullscreen mode Exit fullscreen mode

This tutorial uses swagger-jsdoc version 5.0.1. The latest version might not be compatible with this tutorial.

1.2: Create an API specification

Swagger UI creates a docs page from a set of OpenAPI definitions. These definitions are written in YAML or JSON to describe a REST API. For more information on the basic structure of the OpenAPI Specification, see Basic Structure.

In your Express API's app.js file, add the following code below the list of required modules:

// app.js
const swaggerJSDoc = require('swagger-jsdoc');

const swaggerDefinition = {
  openapi: '3.0.0',
  info: {
    title: 'Express API for JSONPlaceholder',
    version: '1.0.0',
  },
};

const options = {
  swaggerDefinition,
  // Paths to files containing OpenAPI definitions
  apis: ['./routes/*.js'],
};

const swaggerSpec = swaggerJSDoc(options);
Enter fullscreen mode Exit fullscreen mode

The swaggerDefinition object (i.e., the OpenAPI definition) defines the root information for your API. Provide a few basic pieces of information to the swaggerDefinition, such as the title and version of your API; you can fill in more later.

The options object contains this swaggerDefinition object and an array of paths called apis. These are paths to files containing other OpenAPI definitions. These file paths should be relative to the root directory of your Express API. In our case, definitions will be written in JSDoc directly in the /routes files. You can list the filenames individually or use the wildcard delimiter * to add all JavaScript files in a directory, as shown above.

The options object is used by swagger-jsdoc to produce an OpenAPI specification in a variable called swaggerSpec. This specification is equivalent to the swagger.json or swagger.yaml file normally used by Swagger UI to create a docs page. You'll pass this object to Swagger UI in the next step.

Restart the Express server to ensure there are no errors. If you get any errors at this stage, check that your swagger-jsdoc version is 5.0.1 exactly.

1.3: Create the Swagger UI docs page

To create a Swagger UI page for your Express API, include swagger-ui-express in the app.js file. Then, add an endpoint path called /docs (or any name of your choosing):

// app.js
// ...
const swaggerJSDoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');

// ...

var app = express();

app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
Enter fullscreen mode Exit fullscreen mode

As shown above, swagger-ui-express provides two callbacks to set up the endpoint: one to set up Swagger UI with the swaggerSpec definitions and one to serve it to the /docs endpoint.

Restart the Express server, and navigate to localhost:3000/docs in the browser.

You'll see the title and version number of your Express API, as well as the OpenAPI version number (3.0.0). Since we have no other definitions yet, you'll see a "No operations defined in spec!" message:

Basic Swagger UI Page

You now have the start of a beautiful docs page for your API! The rest of this tutorial provides a basic introduction to OpenAPI definitions.

Step 2: Define your API's root information

You've created a Swagger UI docs page, and you're set to start writing docs. But first, you should add more root definitions for the API.

Return to app.js. Note that the info object maps to OpenAPI's Info Object to define a title, description, list of servers, contact information, and list of paths for your API.

Here is an example of a more complete definition:

// app.js
const swaggerDefinition = {
  openapi: '3.0.0',
  info: {
    title: 'Express API for JSONPlaceholder',
    version: '1.0.0',
    description:
      'This is a REST API application made with Express. It retrieves data from JSONPlaceholder.',
    license: {
      name: 'Licensed Under MIT',
      url: 'https://spdx.org/licenses/MIT.html',
    },
    contact: {
      name: 'JSONPlaceholder',
      url: 'https://jsonplaceholder.typicode.com',
    },
  },
  servers: [
    {
      url: 'http://localhost:3000',
      description: 'Development server',
    },
  ],
};
Enter fullscreen mode Exit fullscreen mode

If you have a production server, add the URL and a description to the servers list. See Basic Structure for more information on the other properties you can add to the root definition.

In the OpenAPI docs, you'll notice there's also a paths field. You won't need to specify the path definitions here, since each path is defined separately in a JSDoc comment (to be added in the next step). These path definitions are compiled by swagger-jsdoc into a paths object for you.

Restart the Express server, and navigate again to localhost:3000/docs in the browser. You should see more information about your API at the top of the docs page:

Swagger UI Page with API Information

You can now start documenting your Express routes.

Step 3: Write the docs

With a Swagger UI docs page available at the /docs endpoint and a complete set of root information on your API, you can start writing your path definitions. Each path definition corresponds to an Express route in your API. It describes both the operation and endpoint path, such as GET /users and DELETE /users/:id.

3.1: Document the routes

To document /routes/users.js, first add a comment starting with @swagger above the first route. Follow this with some basic information about the route:

// routes/users.js

/**
 * @swagger
 * /users:
 *   get:
 *     summary: Retrieve a list of JSONPlaceholder users
 *     description: Retrieve a list of users from JSONPlaceholder. Can be used to populate a list of fake users when prototyping or testing an API.
*/
router.get('/', function(req, res) {
  //...
});
Enter fullscreen mode Exit fullscreen mode

Note that swagger-jsdoc looks for comments with a @swagger or @openapi tag to create OpenAPI definitions.

As shown in the code example, add the endpoint path /users and the operation get (indented two spaces). The path in the Express router function get('/') is relative to /users, so the path in the definition should be /users.

The summary should be a brief description of the goal of this route. The description should provide more detail, such as when or why you would want to use the route.

Be sure to use two spaces (or four spaces) for indentation, not tabs. Refer to YAML Syntax for more information.

Restart the Express server, and navigate again to localhost:3000/docs in the browser. You should see a listing for GET /users near the bottom of the page:

Swagger UI Page Showing First Path

3.2: Document responses

Your users will want to know what is returned when this GET request is successful (i.e., with a status code of 200). To define a successful response, add a responses object and a response called 200 to the path definition:

// routes/users.js

/**
 * @swagger
 * /users:
 *   get:
 *     summary: Retrieve a list of JSONPlaceholder users.
 *     description: Retrieve a list of users from JSONPlaceholder. Can be used to populate a list of fake users when prototyping or testing an API.
 *     responses:
 *       200:
 *         description: A list of users.
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 data:
 *                   type: array
 *                   items:
 *                     type: object
 *                     properties:
 *                       id:
 *                         type: integer
 *                         description: The user ID.
 *                         example: 0
 *                       name:
 *                         type: string
 *                         description: The user's name.
 *                         example: Leanne Graham
 */
router.get('/', function(req, res) {
  //...
});
Enter fullscreen mode Exit fullscreen mode

The description field describes the response or what it returns. The content field describes the content type (application/json), and the schema describes the response object. In our case, JSONPlaceholder returns an object with a data field, which contains the data you've requested. For this response, data contains an array of user objects. Add just one or two user properties (e.g., id and name) to avoid cluttering the file for now.

Add a real example value for each property (e.g., 'Leanne Graham'); otherwise, Swagger UI creates a generic example such as 'string'.

Notice how the types are defined in this schema. For example, to define an array, add type: array and an items field. Read more about types in the Data Types documentation.

You can also describe error responses this way. See Swagger's Describing Responses documentation for more details on the fields available for describing each response.

Restart the Express server, and navigate again to localhost:3000/docs in the browser. You should see the response, an example value (using the example values you provided for each property), and the schema for the data returned in this response:

Response and Schema

Next, define the GET /users/:id path by adding the fields we've covered already (summary, description, and responses):

// routes/users.js

 /**
 * @swagger
 * /users/{id}:
 *   get:
 *     summary: Retrieve a single JSONPlaceholder user.
 *     description: Retrieve a single JSONPlaceholder user. Can be used to populate a user profile when prototyping or testing an API.
 *     responses:
 *       200:
 *         description: A single user.
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 data:
 *                   type: object
 *                   properties:
 *                     id:
 *                       type: integer
 *                       description: The user ID.
 *                       example: 0
 *                     name:
 *                       type: string
 *                       description: The user's name.
 *                       example: Leanne Graham
*/

router.get('/:id', function(req, res) {
  //...
});
Enter fullscreen mode Exit fullscreen mode

Here, the path parameter (id) is added to the endpoint path: /users/{id}. Curly brackets ({}) are used to mark a path parameter in the endpoint path. Note that the colon style (/users/:id) won't work with Swagger (thanks @sherwinwater for pointing this out!).

The data object in this schema contains a single user object instead of an array of user objects, but the properties are the same.

Next, define POST /users by adding the fields we've covered already (summary, description, and responses):

// routes/users.js

/**
 * @swagger
 * /users:
 *   post:
 *     summary: Create a JSONPlaceholder user.
 *     responses:
 *       201:
 *         description: Created
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 data:
 *                   type: object
 *                   properties:
 *                     id:
 *                       type: integer
 *                       description: The user ID.
 *                       example: 0
 *                     name:
 *                       type: string
 *                       description: The user's name.
 *                       example: Leanne Graham
*/
router.post('/', function(req, res) {
// ...
});
Enter fullscreen mode Exit fullscreen mode

A successful response in this case would be 201. It returns an object with a data field containing the new user.

You can continue adding path definitions for the remaining routes in the same way. We'll do some refactoring in a later step.

Restart the Express server, and navigate again to localhost:3000/docs in the browser. You'll now see a listing for GET /users/{id}, POST /users, and any other path definitions you've added:

Swagger UI Docs Page

3.3: Document the requests

Request data such as parameters and request bodies can also be documented in your OpenAPI definitions. For example, GET /users/:id has an id parameter, which should be documented.

To document parameters, add a parameters field to the path definition:

// routes/users.js

/**
 * @swagger
 * /users/{id}:
 *   get:
 *     summary: Retrieve a single JSONPlaceholder user.
 *     description: Retrieve a single JSONPlaceholder user. Can be used to populate a user profile when prototyping or testing an API.
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         description: Numeric ID of the user to retrieve.
 *         schema:
 *           type: integer
 *     responses:
 *       200:
 *         ...
 */
router.get('/:id', function(req, res) {
  //...
});

Enter fullscreen mode Exit fullscreen mode

In the definition for this parameter, in defines the parameter's location (in this case, it's a path parameter because it's part of the path). You can also add a name, description, and schema and whether the parameter is required. See Describing Parameters for more details.

Restart the Express server, and navigate again to localhost:3000/docs in the browser. You'll see a list of parameters for this route:

Route Parameters

Next, document the request body for POST /users to describe the data required to create a new user in the database. To do this, add a requestBody field to this path definition:

// routes/users.js

/**
 * @swagger
 * /users:
 *   post:
 *     summary: Create a JSONPlaceholder user.
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             properties:
 *               name:
 *                 type: string
 *                 description: The user's name.
 *                 example: Leanne Graham
 *     responses:
 *       201:
 *         ...
*/
router.post('/', function(req, res) {
// ...
});
Enter fullscreen mode Exit fullscreen mode

This adds a request body schema to this path definition. This example shows that name can be sent in the request body. You can add more properties for new users later. See Describing Request Body for more details.

Restart the Express server, and navigate again to localhost:3000/docs in the browser. You'll see a section called Request body with the schema you've provided:

Request Body

3.4: Document the resources

You might have noticed you've repeated the user schema several times in the documentation so far. To avoid this duplication, you can define the user schema in one place and reference it from elsewhere.

Each model defined by your Express API can be documented separately as a schema definition (or component). To do this for the user model, add a User schema definition to the top of the file, under components/schemas:

// routes/users.js

/**
 * @swagger
 * components:
 *   schemas:
 *     User:
 *       type: object
 *       properties:
 *         id:
 *           type: integer
 *           description: The user ID.
 *           example: 0
 *         name:
 *           type: string
 *           description: The user's name.
 *           example: Leanne Graham
 */
Enter fullscreen mode Exit fullscreen mode

You can then reference this schema definition using $ref:

// routes/users.js

/**
 * @swagger
 * /users:
 *   get:
 *     summary: Retrieve a list of JSONPlaceholder users
 *     description: Retrieve a list of users from JSONPlaceholder. Can be used to populate a list of fake users when prototyping or testing an API.
 *     responses:
 *       200:
 *         description: A list of users.
 *         content:
 *           application/json:
 *             schema:
 *               type: array
 *               items:
 *                 $ref: '#/components/schemas/User'
 */
router.get('/', function(req, res) {
  //...
});
Enter fullscreen mode Exit fullscreen mode

The $ref path uses JSON Reference notation. The # symbol indicates the root of the current document, and the remaining nested values are then resolved in order. For more information, see Using $ref.

Restart the Express server, and navigate again to localhost:3000/docs in the browser. Your path definitions will now use this User schema, and you should see a schema definition for User at the bottom of the page:

User Schema

Similarly, you can define a NewUser object to reference in the POST /users request body. Since it contains some but not all of the fields from the User schema, you can also use $ref to avoid duplication between them:

/**
 * @swagger
 * components:
 *   schemas:
 *     NewUser:
 *       type: object
 *       properties:
 *         name:
 *           type: string
 *           description: The user's name.
 *           example: Leanne Graham
 *     User:
 *       allOf:
 *         - type: object
 *           properties:
 *             id:
 *               type: integer
 *               description: The user ID.
 *               example: 0
 *         - $ref: '#/components/schemas/NewUser'
 */
Enter fullscreen mode Exit fullscreen mode

The allOf keyword combines model definitions, in this case the NewUser definition (containing the name property) and an object with an id property. See oneOf, anyOf, allOf, not for more details.

You can now reference NewUser from the request body definition for POST /users:

/**
 * @swagger
 * /users:
 *   post:
 *     summary: Create a JSONPlaceholder user.
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             $ref: '#/components/schemas/NewUser'
 *     responses:
 *       201:
 *         ...
*/
router.post('/', function(req, res) {
  // ...
});
Enter fullscreen mode Exit fullscreen mode

Restart the Express server, and navigate again to localhost:3000/docs in the browser. You'll see your NewUser schema in the request body definition for POST /users:

NewUser Schema
This covers the basic techniques for producing OpenAPI definitions in JSDoc comments.

Conclusion

You are now set up to produce a complete reference documentation page for your Express API. You've created a basic set of OpenAPI definitions and a Swagger UI page that displays them. If you want more practice with the OpenAPI Specification, you can finish documenting the jsonplaceholder-express-api.

This tutorial has also covered the basics of writing OpenAPI definitions. To complete your documentation, consult the OpenAPI Specification and the Swagger Docs.

To see a version of the jsonplaceholder-express-api that includes all the code added during this tutorial, see the repository's docs branch.