DEV Community

Cover image for How to Document an Express API with Swagger UI and JSDoc
Kate Bartolo
Kate Bartolo

Posted on • Updated on

How to Document an Express API with Swagger UI and JSDoc

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.

Latest comments (37)

Collapse
 
akhil27styles profile image
Akhil Kumar Singh

can we auto-generate the yaml file by using some command?

Collapse
 
abold profile image
Amond Bold

Thank you. Good post!!!

Collapse
 
noone_ profile image
Nurgul

Can I use this method for API projects that are coded in CPP?

Collapse
 
burloiumarian23 profile image
burloiumarian23

Very good article ! Thanks man!

Collapse
 
darkmavis1980 profile image
Alessio Michelini

Really nice and comprehensive article!

Collapse
 
hyeonss0417 profile image
Hyeonss

In 2023, you don't need to do that.

⚠️ The worst down-side of generating OpenAPI with JSDoc is REPEATING yourself for every schemas you made even if some schemas are shared with other APIs. (violating the DRY principle) ⚠️

Tspec automatically parses your TypeScript types and generates up-to-date OpenAPI specification with beautiful Swagger UI.

I strongly recommend you to try it out as I'm a maintainer of Tspec.

Collapse
 
yyyj2813 profile image
yeahhaey

Nice

Collapse
 
vturnus profile image
William Rookwood

Thank you for your greate article,

Would you have or know any article that customize the CSS of the swagger?

Collapse
 
mahbod profile image
Mahbod Ahmadi

Can we use a javascript variable in the swagger comment doc?
I have a json file and stored the responses messages and I wanna use them into doc. How can I do that?

Collapse
 
apanjwani0 profile image
Aman Panjwani

Hi, is there any way to specify schema in a variable form ?
eg-> using joi-tos-wagger(npmjs.com/package/joi-to-swagger).

It would be great if I can just specify swagger components schemas in the validation files itself, would save a lot of time. Or is there any other alternate available ?

PS-: currently there are some articles to use joi-to-swagger, which does the same thing, like -: medium.com/swlh/do-not-repeat-your.... But I'm asking if we can do this with jsdoc.

Thanks.

Collapse
 
mamta92 profile image
Mamta Shettigar

Hi I'm using swagger-jsdoc library. Wanted to know if Visual Studio code offers any extensions for intellisense? There is an extension named swagger-jsdoc but the syntax for post request is quite outdated according to openapi 3.0.0 and hence not able to get automatic intellisense for requestBody..It still uses ;-
parameters:

  • - name: "param_name"
  • description: "매개변수"
  • in: body
  • required: true
  • type: string
  • example: "foo"

Instead of requestBody:

  • required: true
  • content:
  • application/json:
  • schema:
  • $ref: '#/components/schemas/NewUser'
Collapse
 
ave profile image
Alexander • Edited

This is the main drawback of the whole swagger-jsdoc approach to swagger.
Defining API schema as an annotation to your method in the comment section is prone to typos and other mistakes that are not picked up.

I usually go to editor.swagger.io and define the whole API spec there with all the "definitions" schemas and what not. Alternatively, you could use the OpenAPI (Swagger) Editor extension for VS Code ( I don't use it though, so not sure how up-to-date it is).
Then it is a matter of copying pieces of it into right places in the code.
Also, note that you don't have to define the common definitions and schemas in the annotation section in your code. It is cleaner to put them into a separate yaml file, say definitions.yaml, then load the file directly into swaggerJSDoc object:

const options = {
  swaggerDefinition,
  // Paths to files containing OpenAPI definitions
  apis: ['./routes/*.js', './doc/definitions.yaml'],
};
Enter fullscreen mode Exit fullscreen mode
Collapse
 
burloiumarian23 profile image
burloiumarian23 • Edited

Good hint ... it wasn't working initially for me , because I also added anotation paths which was wrong

Collapse
 
rizkyrajitha profile image
Rajitha Gunathilake

thanks for the article
this was really helpful .👌

Collapse
 
sherwinwater profile image
Shuwen

GET /users/:id doesn't work for me. I change it to GET /users/{id}, which works.

In the api-docs, once I input id number and click execute, here is the difference:

/users/{id}
curl -X GET "localhost:3000/users/1" -H "accept: application/json"

/users/:id
curl -X GET "localhost:3000/users/:id" -H "accept: application/json"

Collapse
 
kabartolo profile image
Kate Bartolo

Hi Shuwen!
I'm really glad you pointed this out because it's totally a bug. I'm not sure why I thought :id worked, but you're right that it doesn't.

I'll update the tutorial and sample project. Thank you!

Collapse
 
sherwinwater profile image
Shuwen

Hi Kate, Thank you your article to help me setting up my swagger documentation. One of the best article I find and I've recommend it to my coworkers.

Thread Thread
 
kabartolo profile image
Kate Bartolo

Thank you so much! I'm happy it was helpful

Collapse
 
habereder profile image
Raphael Habereder

Good find! And also, welcome to DEV :)

As far as I know, swagger implements {} as template delimiters, not colons, but I'm not using node a lot (yet), so maybe I am missing something here.
I can't find any information in the swagger-docs that mention the colon-style, so input from @kabartolo would be great here :)

Collapse
 
sherwinwater profile image
Shuwen

This is the article I am looking for. Thank you!

Collapse
 
miteshkamat27 profile image
Mitesh Kamat

Thanks for sharing. I wasn't aware of this.

Collapse
 
rolfstreefkerk profile image
Rolf Streefkerk

There's another way of doing this with tools provided by Swagger.io you can have it load in the OpenAPI yaml document, and it will generate the HTML specification document for you.

github.com/swagger-api/swagger-cod...

Collapse
 
kabartolo profile image
Kate Bartolo

This is good to know, thank you!