DEV Community

Cover image for How to write Interactive API Documentation ?
Himanshu Aggarwal
Himanshu Aggarwal

Posted on

How to write Interactive API Documentation ?

API Documentation Overview

An API serves as a mediator between applications and web servers, facilitating information sharing. Properly documenting APIs is crucial for a smooth developer experience. API documentation is akin to a user manual, providing guidance on using and integrating with an API effectively.

api meme

It should cover:

  • API overview and its purpose
  • Step-by-step tutorials and examples
  • Glossary of key terms
  • Supported operations
  • API response details

meme

This article introduces Swagger, an ecosystem of tools for streamlined API development and documentation using the OpenAPI Specification (OAS).

What is OAS?

OpenAPI specification (previously known as Swagger specification) describes a standard format for REST APIs. This standard is important so that everyone who writes REST APIs is compliant with its best practices such as the versioning, safety, and error handling. We can say that OpenAPI is similar to a template with a set of rules and constraints explaining how you could describe an API. It is usually written in YAML or JSON file format making it readable for humans and machines.

This file let us:-

  • Describe all available API endpoints (e.g. /users, /users/{id})
  • Operations in the endpoints such as GET /users, POST /user
  • Input and Output parameters for each operation
  • Authentication mechanisms
  • Contact information, API Licence, terms of use and other information

meme

What is Swagger?

Swagger is a set of open-source tools built around the OpenAPI Specification that can help us to design, build, document and consume REST APIs. The ability of APIs to describe their own structure is the root of all awesomeness in Swagger. Note that Swagger not only helps us to design and document REST APIs, it also lets us build (Server Stub) and Consume (Rest Clients) REST APIs.

The major Swagger tools include:

  • Swagger Editor — browser-based editor where one can write OpenAPI specification
  • Swagger UI — renders OpenAPI specs as interactive API documentation
  • Swagger Codegen — generates server stubs and client libraries from an OpenAPI specification

swagger logo

Express.js API application setup

Set Up your NodeJS,Express App with all the API Endpoints
Once you have done it, run the commands below in the terminal:

npm install
npm i swagger-ui-express swagger-jsdoc
Enter fullscreen mode Exit fullscreen mode

These are going to download the required dependencies into your application. Now it’s time to integrate Swagger into your Node.js application.

How to connect Swagger to Node.js?

To connect Swagger to your Node.js application, import swagger-ui-express and swagger-jsdoc in your server.js or create a seprate swagger.js file for the :

  const swaggerJsdoc = require("swagger-jsdoc"),
  const swaggerUi = require("swagger-ui-express");

Enter fullscreen mode Exit fullscreen mode

Those are the two respective objects representing the libraries we’ve imported. Next, add the following code before the app’s listen function:

const options = {
  definition: {
    openapi: '3.1.0',
    info: {
      title: 'CRUD API Documentation',
      version: '1.0.0',
      description: 'Documentation for the CRUD API created using Node.js, Express, and MongoDB.',
      contact: {
        name: "Himanshu Aggarwal",
        email: "himanshuaggar00@gmail.com",
    },
    license: {
      name: "MIT",
      url: "https://spdx.org/licenses/MIT.html",
    },
    },
    servers: [
      {
        url: 'http://localhost:9000', // Change the URL based on your server configuration
        description: 'Local server',
      },
    ],
  },
  apis: ['./routes/*.js'], // Path to the API routes directory
};

const specs = swaggerJsdoc(options);

module.exports = {
  serve: swaggerUi.serve,
  setup: swaggerUi.setup(specs),
};

Enter fullscreen mode Exit fullscreen mode

As you see in the first line, this configuration object sets an OpenAPI to v3.1.0.

Swagger uses the Open API Specification, a standard, language-agnostic interface for RESTful APIs that allows humans and machines to understand the capabilities of a web service without having to access the source code or inspect the network traffic.

You can refer to the official docs for all available settings for each version. Here, we’re using just the basics: API info, name, title, description, license, the contact of the API owner, and more.

The API’s property is essential because it searches for the model and endpoint definitions, so inform it correctly.

Finally, we’re using the swaggerJsdoc function to scan through the options passed in as a param and return the converted Swagger specification object. This one, in turn, can be used along with the swaggerUi setup process.

You can now start the application via the npm start command. You’ll see the following screen when accessing the http://localhost:9000/api-docs/

swagger img

Note, that we still don’t have any operations defined in the spec. This happens because we need to map those operations to the routes explicitly. Otherwise, Swagger can’t figure out the API endpoints on its own.

Optionally, you can add a search bar to your UI just in case your API has too many operations. For this, change the implementation to the following:

app.use(
  "/api-docs",
  swaggerUi.serve,
  swaggerUi.setup(specs, { explorer: true })
);
Enter fullscreen mode Exit fullscreen mode

Now, the search bar will show up.

Creating the API model

Like many significant frameworks and API architectures, data is encapsulated into models to become more easily accessible. Swagger also expects your APIs to have models and for you to define them.

Go to routes and you can define the schema at the beginning of the file as shown below:


/**
 * @swagger
 * components:
 *   schemas:
 *     Product:
 *       type: object
 *       required:
 *         - name
 *         - brand
 *         - ram
 *         - camera
 *         - network
 *         - fingerprint
 *         - price
 *       properties:
 *         _id:
 *           type: string
 *           description: The auto-generated unique identifier of the product.
 *         name:
 *           type: string
 *           description: The name of the product.
 *         brand:
 *           type: string
 *           description: The brand of the product.
 *         ram:
 *           type: integer
 *           description: The RAM capacity of the product in gigabytes.
 *         camera:
 *           type: string
 *           description: The camera specification of the product.
 *         network:
 *           type: string
 *           description: The network connectivity of the product.
 *         fingerprint:
 *           type: boolean
 *           description: Indicates whether the product has a fingerprint sensor.
 *         price:
 *           type: number
 *           description: The price of the product.
 *       example:
 *         _id: "65d34a7cbe5329a2e035c756"
 *         name: Galaxy S21
 *         brand: Samsung
 *         ram: 8
 *         camera: 64 MP
 *         network: 5G
 *         fingerprint: true
 *         price: 799
 */

Enter fullscreen mode Exit fullscreen mode

Remember the JSDocs we’ve talked about? JSDocs now enters the scene and helps us to set up the rest of the Swagger spec definitions through the@swagger annotation. Here, you can define as many schemas as you want. In our case, we’re just defining the domain Books.

The required property receives the list of attributes that must be filled in the requests. This step is essential for letting people know what they must send when using your API.

The properties property describes detailed information about your model attributes. Each attribute must have a name followed by its type, description (optional), and format (you can validate values too). Please refer to Swagger data types for a complete list of available data types.

Finally, you can provide an example of request data for this schema model. That’s going to be useful later. When you restart the app and refresh the page, you’ll see the screen below:

schema

It’s important to note that if you’re facing a YAMLSemanticError in the schema, make sure to check the indentation in the YAMLconfiguration

Integrating operations into the routes

Now, we have Swagger schema integrated into the routes. However, we still don’t have any operations. Let’s fix that. Right after the previous JSDoc comment, add the following:

/**
 * @swagger
 * tags:
 *   name: Products
 *   description: TheProducts CRUD API
 * /products:
 *   get:
 *     summary: Retrieve a list of all products.
 *     description: Retrieve a list of all products from the database.
 *     responses:
 *       200:
 *         description: A list of products.
 *         content:
 *           application/json:
 *             schema:
 *               type: array
 *               items:
 *                 $ref: '#/components/schemas/Product'
 */
Enter fullscreen mode Exit fullscreen mode

Let’s analyze it in parts, starting with the Swagger tags. A tag allows you to create a section within the Swagger docs. All the routes assigned to this tag will appear under the same division. It’s an organizational setting.

In our example, all the endpoints will be mapped to the same tag. Next, we set up our first route: Retrive Products. It’s pretty straightforward. First, define a title and specify the tag to which the path will be attached.

Then, we have the request and the response. Within the request, define three things: whether the request is required, the content type of the request, and the schema from which it must be processed.

The schemas can be referenced through the #components/schemas Swagger operator. As for the response, define the HTTP response codes and the properties for each of them. We’re just worried about the happy path with an HTTP 200.

routes
You can test the new operation directly within the Swagger UI page

Now, you can see where the example values take place. It’s easier to provide your users with sample data as a reference for when they want to perform stuff.

*Entire code of the Routes *

const express = require("express");
const router = express.Router();
const Product = require('../models/product');

/**
 * @swagger
 * components:
 *   schemas:
 *     Product:
 *       type: object
 *       required:
 *         - name
 *         - brand
 *         - ram
 *         - camera
 *         - network
 *         - fingerprint
 *         - price
 *       properties:
 *         _id:
 *           type: string
 *           description: The auto-generated unique identifier of the product.
 *         name:
 *           type: string
 *           description: The name of the product.
 *         brand:
 *           type: string
 *           description: The brand of the product.
 *         ram:
 *           type: integer
 *           description: The RAM capacity of the product in gigabytes.
 *         camera:
 *           type: string
 *           description: The camera specification of the product.
 *         network:
 *           type: string
 *           description: The network connectivity of the product.
 *         fingerprint:
 *           type: boolean
 *           description: Indicates whether the product has a fingerprint sensor.
 *         price:
 *           type: number
 *           description: The price of the product.
 *       example:
 *         _id: "65d34a7cbe5329a2e035c756"
 *         name: Galaxy S21
 *         brand: Samsung
 *         ram: 8
 *         camera: 64 MP
 *         network: 5G
 *         fingerprint: true
 *         price: 799
 */

/**
 * @swagger
 * /products:
 *   get:
 *     summary: Retrieve a list of all products.
 *     description: Retrieve a list of all products from the database.
 *     responses:
 *       200:
 *         description: A list of products.
 *         content:
 *           application/json:
 *             schema:
 *               type: array
 *               items:
 *                 $ref: '#/components/schemas/Product'
 */
router.get('/', async (req, res) => {
  try {
    const products = await Product.find();
    res.json(products);
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});

/**
 * @swagger
 * /products/{id}:
 *   get:
 *     summary: Retrieve a product by ID.
 *     description: Retrieve a product from the database by its ID.
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         description: ID of the product to retrieve.
 *         schema:
 *           type: string
 *     responses:
 *       200:
 *         description: The requested product.
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/Product'
 *       404:
 *         description: Product not found.
 */
router.get('/:id', getProduct, (req, res) => {
  res.json(res.product);
});

/**
 * @swagger
 * /products:
 *   post:
 *     summary: Create a new product.
 *     description: Create a new product and save it to the database.
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             $ref: '#/components/schemas/Product'
 *     responses:
 *       201:
 *         description: The created product.
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/Product'
 *       400:
 *         description: Invalid product data.
 */
router.post('/', async (req, res) => {
  const product = new Product({
    name: req.body.name,
    brand: req.body.brand,
    ram: req.body.ram,
    camera: req.body.camera,
    network: req.body.network,
    fingerprint: req.body.fingerprint,
    price: req.body.price
  });

  try {
    const newProduct = await product.save();
    res.status(201).json(newProduct);
  } catch (error) {
    res.status(400).json({ message: error.message });
  }
});

/**
 * @swagger
 * /products/{id}:
 *   put:
 *     summary: Update a product by ID.
 *     description: Update an existing product in the database by its ID.
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         description: ID of the product to update.
 *         schema:
 *           type: string
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             $ref: '#/components/schemas/Product'
 *     responses:
 *       200:
 *         description: The updated product.
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/Product'
 *       404:
 *         description: Product not found.
 */
router.put('/:id', getProduct, async (req, res) => {
  try {
    const updatedProduct = await res.product.set(req.body).save();
    res.json(updatedProduct);
  } catch (error) {
    res.status(400).json({ message: error.message });
  }
});

/**
 * @swagger
 * /products/{id}:
 *   delete:
 *     summary: Delete a product by ID.
 *     description: Delete a product from the database by its ID.
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         description: ID of the product to delete.
 *         schema:
 *           type: string
 *     responses:
 *       200:
 *         description: Product deleted successfully.
 *       404:
 *         description: Product not found.
 */
router.delete('/:id', getProduct, async (req, res) => {
  try {
    await res.product.remove();
    res.json({ message: 'Product deleted successfully' });
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});

async function getProduct(req, res, next) {
  try {
    const product = await Product.findById(req.params.id);
    if (product == null) {
      return res.status(404).json({ message: 'Product not found' });
    }
    res.product = product;
    next();
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
}

module.exports = router;

Enter fullscreen mode Exit fullscreen mode

all routes

Conclusion

You may test each endpoint individually to ensure it’s working precisely as your Postman requests.

Swagger is capable of way more than merely documenting your APIs. A quick read over the official docs will give you a better understanding of its power. Remember that documenting should be part of your Project.

pic

Top comments (0)