Documenting Node.js API using Swagger

desmondsanctity

Desmond Obisi

Posted on March 20, 2023

Documenting Node.js API using Swagger

Introduction

API documentation is an integral part of developing software. It is an instruction manual that explains how to use an API and its services. This manual might contain tutorials, code examples, screenshots, and anything else that helps users better understand how to work with the API.

In this article, we will be learning how to document API written in Node.js using a tool called Swagger. Swagger allows you to describe the structure of your APIs so that machines can read them. The ability of APIs to describe their own structure is the root of all awesomeness in Swagger. Why is it so great? Well, by reading our API’s structure, swagger can automatically build beautiful and interactive API documentation. It can also automatically generate client libraries for your API in many languages and explore other possibilities like automated testing. Swagger does this by asking our API to return a YAML or JSON that contains a detailed description of your entire API. This file is essentially a resource listing of our API which adheres to OpenAPI Specifications.

Building our API with Node.js and Express

To start writing the API specs, we will build our API with Node.js which is a back-end JavaScript runtime environment that runs on the V8 JavaScript Engine and executes JavaScript code outside a web browser. For the purpose of simplicity, I have set up the project and it can be cloned from this GitHub repository. To get the backend running on our local machine, we will follow these steps:

  • Create a new folder for the project and run this command in the root folder to clone the repository
git clone https://github.com/DesmondSanctity/node-js-swagger.git
Enter fullscreen mode Exit fullscreen mode
  • To successfully run the code, we will need a database connection. I used a MongoDB Atlas cluster for the database and we can follow this tutorial to set up one, it is quite simple to set up. After the set up, we will get our URL and that is all we need to connect to our database from our app.
  • We are using JSON Web Token (JWT) to authenticate access to our API, so we will generate a secret key that will be used by our app to send and verify requests. To generate that, we will run this command anywhere in our terminal. This script will generate a random 64-bit ASCII string, that can be used for encrypting JWT tokens.
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Enter fullscreen mode Exit fullscreen mode
  • We will now create a file named .env where we will store our MongoDB Atlas cluster URL and JWT secret as an environment variable. The file should look like this:
    JWT_SECRET=<your JWT secret>
    ATLAS_URI=<your MongoDB Atlas cluster URL>
Enter fullscreen mode Exit fullscreen mode
  • Now, we are ready to run the application but first, we will install some packages and then start the application. If you cloned the GitHub repository earlier, you just need to run these commands below:
    npm install   // To install the neccessary packages
    npm start    //  To start the application
Enter fullscreen mode Exit fullscreen mode
  • If you are successful at this point, you will see the following message in your terminal
    > mini-blog@1.0.0 start
    > nodemon server.js

    [nodemon] 2.0.20
    [nodemon] to restart at any time, enter `rs`
    [nodemon] watching path(s): *.*
    [nodemon] watching extensions: js,mjs,json
    [nodemon] starting `node server.js`
    Database Connected
    Server connected to http://localhost:8080
Enter fullscreen mode Exit fullscreen mode

Adding Swagger UI and Configurations

Now that we have our API ready, we will start defining the Swagger specs for them. There are two ways we can build Swagger documentation for our API:

  • Manually write the specs inside the router files or a dedicated json or yaml file in our application.
  • Using existing developer’s tools or packages to generate the documentation automatically.

In this tutorial, we will use the manual approach to ensure accuracy in our definitions and specifications. First, we will install two packages called "swagger-jsdoc" and "swagger-ui-express" as dependencies using this command:

npm install swagger-jsdoc swagger-ui-express --save-dev
Enter fullscreen mode Exit fullscreen mode

After the installation, we will create a new file called swagger.js in the root directory of our application and paste the following code into it

    import swaggerJsdoc from 'swagger-jsdoc'
    import swaggerUi from 'swagger-ui-express'
    const options = {
      definition: {
        openapi: '3.0.0',
        info: {
          title: 'Mini Blog API',
          description: "API endpoints for a mini blog services documented on swagger",
          contact: {
            name: "Desmond Obisi",
            email: "info@miniblog.com",
            url: "https://github.com/DesmondSanctity/node-js-swagger"
          },
          version: '1.0.0',
        },
        servers: [
          {
            url: "http://localhost:8080/",
            description: "Local server"
          },
          {
            url: "<your live url here>",
            description: "Live server"
          },
        ]
      },
      // looks for configuration in specified directories
      apis: ['./router/*.js'],
    }
    const swaggerSpec = swaggerJsdoc(options)
    function swaggerDocs(app, port) {
      // Swagger Page
      app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec))
      // Documentation in JSON format
      app.get('/docs.json', (req, res) => {
        res.setHeader('Content-Type', 'application/json')
        res.send(swaggerSpec)
      })
    }
    export default swaggerDocs
Enter fullscreen mode Exit fullscreen mode

We can see from the code where we defined the Open API Specification (OAS) that we will be using for our documentation: the information or details about the API, the servers we will expose it to and the routes in our app where Swagger should look for specifications for each of our API.

We can also see our swaggerDocs function that allows the app instance and port to be able to generate the documentation for using the swaggerUi and swaggerJsdoc package we installed earlier and serve it at /docs route. We can also get the JSON format using /docs.json. Lastly, we will update our server.js file to include our swaggerDocs function to always generate and update our docs whenever we run the project.

The final step before writing out the specification for each endpoint is to add the swaggerDocs function to our server.js file so we initiate swagger whenever our app starts.

    import express from 'express';
    import cors from 'cors';
    import morgan from 'morgan';
    import dotenv from 'dotenv';
    import connect from './database/conn.js';
    import userRouter from './router/user.js';
    import postRouter from './router/post.js';
    import swaggerDocs from './swagger.js'

    dotenv.config()
    const app = express();
    /** middlewares */
    app.use(express.json());
    app.use(cors());
    app.use(morgan('tiny'));
    app.disable('x-powered-by'); // less hackers know about our stack

    const port = process.env.PORT || 8080;
    /** HTTP GET Request */
    app.get('/', (req, res) => {
        res.status(201).json("Home GET Request");
    });

    /** api routes */
    app.use('/api/user', userRouter)
    app.use('/api/post', postRouter)
    /** start server only when we have valid connection */
    connect().then(() => {
        try {
            app.listen(port, () => {
                console.log(`Server connected to http://localhost:${port}`);
            })
            swaggerDocs(app, port)
        } catch (error) {
            console.log('Cannot connect to the server')
        }
    }).catch(error => {
        console.log("Invalid database connection...!");
    })
Enter fullscreen mode Exit fullscreen mode

Writing our API Specifications

Now to the main task, we will write the specifications for our API that Swagger will use to generate the documentation for us. We currently have two controllers and router files in our app for user and post. Our controllers contain logic for the app while the router carries the endpoint and payload to the controller in form of requests. Let’s start defining the spec!

For User router, we will first import the packages we will be needing in the router file user.js:

    import { Router } from "express";
    const userRouter = Router();
    /** import all controllers */
    import * as controller from '../controllers/userController.js';
    import Auth from '../middleware/auth.js';

    /** All the API routes comes here*/

    export default userRouter;
Enter fullscreen mode Exit fullscreen mode

Then we will write the spec for each request type viz GET, POST, PUT and DELETE. The specification is a yaml file embedded at the beginning of the route we want to document. Some key points to note are:

  • Open API specification instance—written at the beginning of the yaml file.
  • The API endpoint—the URL we will use for the request.
  • Request Type—indicates if it is a GET, POST, PUT or DELETE request.
  • Request Body—for passing our payload to the API.
  • Parameters—data we pass through the URL or params to the backend
  • Content Type—the type of content we are sending. It is passed to the HTTP header.
  • Schema—it contains the type of our request body, the required fields and the properties that the request body can accept.
  • Response—the result of the API call we made, it tells us if it failed or succeded and reports errors as well.

POST:

    /** POST Methods */
    /**
     * @openapi
     * '/api/user/register':
     *  post:
     *     tags:
     *     - User Controller
     *     summary: Create a user
     *     requestBody:
     *      required: true
     *      content:
     *        application/json:
     *           schema:
     *            type: object
     *            required:
     *              - username
     *              - email
     *              - password
     *            properties:
     *              username:
     *                type: string
     *                default: johndoe 
     *              email:
     *                type: string
     *                default: johndoe@mail.com
     *              password:
     *                type: string
     *                default: johnDoe20!@
     *     responses:
     *      201:
     *        description: Created
     *      409:
     *        description: Conflict
     *      404:
     *        description: Not Found
     *      500:
     *        description: Server Error
     */
    userRouter.route('/register').post(controller.register); // register user

    /**
     * @openapi
     * '/api/user/login':
     *  post:
     *     tags:
     *     - User Controller
     *     summary: Login as a user
     *     requestBody:
     *      required: true
     *      content:
     *        application/json:
     *           schema:
     *            type: object
     *            required:
     *              - username
     *              - password
     *            properties:
     *              username:
     *                type: string
     *                default: johndoe
     *              password:
     *                type: string
     *                default: johnDoe20!@
     *     responses:
     *      201:
     *        description: Created
     *      409:
     *        description: Conflict
     *      404:
     *        description: Not Found
     *      500:
     *        description: Server Error
     */
    userRouter.route('/login').post(controller.verifyUser,controller.login); // login in app

    /**
     * @openapi
     * '/api/user/verify':
     *  post:
     *     tags:
     *     - User Controller
     *     summary: Verify a user
     *     requestBody:
     *      required: true
     *      content:
     *        application/json:
     *           schema:
     *            type: object
     *            required:
     *              - username
     *            properties:
     *              username:
     *                type: string
     *                default: johndoe
     *     responses:
     *      201:
     *        description: Created
     *      409:
     *        description: Conflict
     *      404:
     *        description: Not Found
     *      500:
     *        desccription: Server Error
     */
    userRouter.route('/verify').post(controller.verifyUser, (req, res) => res.end()); // authenticate user
Enter fullscreen mode Exit fullscreen mode

GET:

    /** GET Methods */
    /**
     * @openapi
     * '/api/user/{username}':
     *  get:
     *     tags:
     *     - User Controller
     *     summary: Get a user by username
     *     parameters:
     *      - name: username
     *        in: path
     *        description: The username of the user
     *        required: true
     *     responses:
     *      200:
     *        description: Fetched Successfully
     *      400:
     *        description: Bad Request
     *      404:
     *        description: Not Found
     *      500:
     *        description: Server Error
     */
    userRouter.route('/:username').get(controller.getUser) // user with username
Enter fullscreen mode Exit fullscreen mode

PUT:

/** PUT Methods */
    /**
     * @openapi
     * '/api/user/update':
     *  put:
     *     tags:
     *     - User Controller
     *     summary: Modify a user
     *     requestBody:
     *      required: true
     *      content:
     *        application/json:
     *           schema:
     *            type: object
     *            required:
     *              - userId
     *            properties:
     *              userId:
     *                type: string
     *                default: ''
     *              firstName:
     *                type: string
     *                default: ''
     *              lastName:
     *                type: string
     *                default: ''
     *     responses:
     *      200:
     *        description: Modified
     *      400:
     *        description: Bad Request
     *      404:
     *        description: Not Found
     *      500:
     *        description: Server Error
     */
    userRouter.route('/update').put(controller.updateUser); // is use to update the user profile
Enter fullscreen mode Exit fullscreen mode

DELETE:

/** DELETE Methods */
    /**
     * @openapi
     * '/api/user/{userId}':
     *  delete:
     *     tags:
     *     - User Controller
     *     summary: Delete user by Id
     *     parameters:
     *      - name: userId
     *        in: path
     *        description: The unique Id of the user
     *        required: true
     *     responses:
     *      200:
     *        description: Removed
     *      400:
     *        description: Bad request
     *      404:
     *        description: Not Found
     *      500:
     *        description: Server Error
     */
    userRouter.route('/:userId').delete(controller.deleteUser);
Enter fullscreen mode Exit fullscreen mode

Testing our APIs on Swagger

After finishing our API documentation, we should be able to view our Swagger documentation and test our API using it as well. If you followed till this point, you should have a view like the one below. Our documentation is served on the /docs route.

Swagger UI documentation

We will make some requests using the Swagger documentation UI and see the results.

To create a User:

A POST request to create a user

To create a Post:

A POST request to create a post

As we have seen from the examples above, we can use the Swagger documentation to test our API, to create, read and modify data in our database. This helps to make the API understandable and easy to integrate by others. Following these examples, we can go ahead and test for other request methods like PUT to update a user or post, GET to read a user or post and DELETE to delete them from the database.

Conclusions

We can finally conclude that API documentation is a very important part of the software development cycle and that it aids collaboration and makes user experience seamless. Some of the advantages of Swagger include but are not limited to:

  • Synchronizes the API documentation with the server and client at the same pace.
  • Allows us to generate REST API documentation and interact with the REST API. The interaction with the REST API using the Swagger UI Framework gives clear insight into how the API responds to parameters.
  • Provides responses in the format of JSON and XML.
  • Implementations are available for various technologies.
💖 💪 🙅 🚩
desmondsanctity
Desmond Obisi

Posted on March 20, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Documenting Node.js API using Swagger
javascript Documenting Node.js API using Swagger

March 20, 2023