DEV Community

Sagacité
Sagacité

Posted on

DOCUMENTING YOUR API WITH SWAGGER

Ever gone through the engineering your API's knowing fully well, you will need to come up with a documentation so other devs can work with it?

You have a short answer here. I will try to make this docs as simple and comprehensive as it might be. Might not be as comprehensive as you want, but you willl be able to scale it up to meet your demands.

Stacks and technologies needed: Node.js, Express, MongoDB, Swagger.

Initialize a project using npm init.

Follow the step by step guideline/prompt...

You can go ahead to install express and Mongo and some other dependecies necessary

npm i express mongoose nodemon dotenv cors

  • Express will be used for the middleware to create various CRUD endpoints.
  • Mongoose for managing data in MongoDB.
  • Nodemon to restart our server every time we save our file.
  • Dotenv to manage a .env file.
  • cors to manage and provide a middleware that can be used to enable CORS with various options

Once the installation is complete, go ahead to create an index.js in the root directory of your application.

const express = require('express');
const mongoose = require('mongoose');

const app = express();

app.use(express.json());

app.listen(3000, () => {
    console.log(`Server Started at ${3000}`)
})
Enter fullscreen mode Exit fullscreen mode

Things to note from the code lines above:
We have imported express and mongoose as dependencies and assigned express to app.
Now we can listen on the port we assigned once we start the server.

In package.json file, you can add the line below:

"scripts": {
    "start": "nodemon index.js"
},
Enter fullscreen mode Exit fullscreen mode

Remember Nodemon will help us auto-restart the server on code changes.

You can just do a npm start to start your node server.

We won't be going deep into getting our MongoDB ready and set up in this tutorial course. You can check this out from this section.io post
** I will be using MongoDB Compass which allows for local development and testing.

After setting up our DB, we can create a .env file and add our DB details.

MONGO_DB=mongodb://localhost:27017/
PORT=3000
Enter fullscreen mode Exit fullscreen mode

The value for MONGO_DB in your .env might be different depending on the MongoDB URL you get (compass/atlas).

Now we can modify index.js file to effect some needed changes.

require("dotenv").config();
const cors = require("cors");
const express = require("express");

const { connect } = require("mongoose");
const app = express();

app.use(cors());
app.use(express.urlencoded({ extended: false }));
app.use(express.json());

const runApp = async () => {
    try {
        await connect(process.env.MONGO_DB, {
            useUnifiedTopology: true,
            useNewUrlParser: true,
        });
        console.log(`Successfully connected to database ${process.env.MONGO_DB}`);
        app.listen(process.env.PORT, () => {
            console.log(`Server started successfully on PORT ${process.env.PORT}`);
        })
    } catch(err) {
        console.log(err);
        runApp();
    }
};

runApp();

Enter fullscreen mode Exit fullscreen mode

Using dotenv allows for .env variables to be accessible on this page.
In the try block, we put our .env variables to use there.

Running npm start should give us this result below

Image description

If you get that result, congratulations. Let us go ahead and install our swagger dependecies.

npm i swagger-autogen swagger-ui-express

Creating Routes for our endpoints

Create a folder called routes and inside, make a file called index.js
Before proceeding, let's import this file into our main script file.

Our main script file should look like this now:

require("dotenv").config();
const cors = require("cors");
const express = require("express");

const { connect } = require("mongoose"); 
const app = express();
const router = require("./routes/index");

app.use(cors());
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
app.use(router);

const runApp = async () => {
    try {
        await connect(process.env.MONGO_DB, {
            useUnifiedTopology: true,
            useNewUrlParser: true,
        });
        console.log(`Successfully connected to database ${process.env.MONGO_DB}`);
        app.listen(process.env.PORT, () => {
            console.log(`Server started successfully on PORT ${process.env.PORT}`);
        })
    } catch(err) {
        console.log(err);
        runApp();
    }
};

runApp();

Enter fullscreen mode Exit fullscreen mode

We will be getting an error now because our routes/index.js file is empty.

** We are going to create a simple student database, no authentication or authorization needed.

Let's create a students.js file in our routes folder.
Add the following content:

    const router = require("express").Router();

    router.get("/students", async (req, res) => {
        res.send('Get All students')
    })

    //Post 
    router.post('/students', async (req, res) => {
        res.send('Add a new student')
    })

    //Get by ID 
    router.get('/students/get/:id', async (req, res) => {
        res.send('Get by one student by ID')
    })

    //Update by ID 
    router.patch('/students/update/:id', async (req, res) => {
        res.send('Update student by ID')
    })

    //Delete by ID 
    router.delete('/students/delete/:id', async (req, res) => {
        res.send('Delete by ID')
    })


    module.exports = router
Enter fullscreen mode Exit fullscreen mode

Now we can go back to our routes/index.js file to import our students file. This method allows us to define different routes for different collections in our DB.

We have five methods that use the REST Methods for our CRUD operation.

This router is taking the route as the first parameter. Then in the second parameter it's taking a callback.

In the callback, we have a res and a req. res means response, and req means request. We use res for sending responses to our client, like Postman, or any front-end client. And we use req for receiving requests from a client app like Postman, or any front-end client.

Then in the callback body, we are printing a message that says the respective API message.

Let's populate our routes/index.js with code below:

const router = require("express").Router();

const studentRoutes = require('./students.js');

router.use("/api", studentRoutes); 

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

Checking our server instance now, there won't be any process if you followed through to this point.

Creating Models for our DB structure

Create a folder called model and inside, a file called student.js.

const mongoose = require('mongoose');

const studentSchema = new mongoose.Schema({
    firstname: {
        required: true,
        type: String
    },
    lastname: {
        required: true,
        type: String
    },
    middlename: {
        required: false,
        type: String
    },
    class: {
        required: true,
        type: String
    },
    age: {
        required: true,
        type: Number
    }
})

module.exports = mongoose.model('Student', studentSchema)
Enter fullscreen mode Exit fullscreen mode

Here, we have a schema that defines our database structure. It has some properties. The fields have types with some required and some optional.

Creating Controllers for our APIs

Create a folder called controllers. This helps abstract our functions and make out routes files less bulky and buggy

We can then create a students.js file in the controllers folder

const Student = require("../model/students.js");

const addOne = async(req, res) => {
    try {
        const newRecord = new Student({
            ...req.body,
        });
        await newRecord.save();
        return res.status(201).json({
            message: "Student successfully created",
            success: true
        });
    } catch(err) {
        return res.status(500).json({
            message: err.message,
            success: false,
        });
    }
};

const removeOne = async(req, res) => {
    try {
        const deleted = await Student.findByIdAndDelete(req.params.id);
        if(!deleted) {
            return res.status(404).json({
                message: "Student not found",
                success: false,
            });
        }
        return res.status(204).json({
            message: "Student successfully deleted",
            success: true,
        });
    } catch(err) {
        return res.status(500).json({
            message: err.message,
            success: false,
        });
    }
};

const updateOne = async(req, res) => {
    try {
        await Student.findByIdAndUpdate(req.params.id, req.body);
        return res.status(201).json({
            message: "Student's data successfully updated",
            success: true,
        });
    } catch(err) {
        return res.status(500).json({
            message: err.message,
            success: false,
        });
    }
};

const getAll = async(req, res) => {
    try {
        const [results, studentCount] = await
        Promise.all([
            Student.find({}) 
                .sort({createdAt: -1})
                .limit(req.query.limit)
                .skip(req.skip)
                .lean()
                .exec(),
                Student.count({}),
        ]);
        const pageCount = Math.ceil(studentCount / req.query.limit);
        return res.status(201).json({
            data: results,
            pageCount,
            studentCount,
        });
    } catch(err) {
        return res.status(500).json({
            message: err.message,
            success: false,
        });
    }
};

const getOne = async(req, res) => {
    try {
        const student = await Student.findById(req.params.id);
        if(student) {
            return res.status(200).json(student);
        }
        return res.status(404).json({
            message: "Student not found",
            success: false,
        });
    } catch(err) {
        return res.status(500).json({
            message: err.message,
            success: false,
        });
    }
};

module.exports = {
    addOne,
    removeOne,
    updateOne,
    getAll,
    getOne
}
Enter fullscreen mode Exit fullscreen mode

Now, we can go back to routes/students.js to make use of these functions.


    const router = require("express").Router();
    const { addOne, removeOne, updateOne, getAll, getOne } = require("../controller/students.js");


    router.get("/students", async (req, res) => {
        await getAll(req, res);
    })

    //Post 
    router.post('/students', async (req, res) => {
        await addOne(req, res);
    })

    //Get by ID 
    router.get('/students/get/:id', async (req, res) => {
        await getOne(req, res);
    })

    //Update by ID 
    router.patch('/students/update/:id', async (req, res) => {
        await updateOne(req, res);
    })

    //Delete by ID 
    router.delete('/students/delete/:id', async (req, res) => {
        await removeOne(req, res);
    })


    module.exports = router
Enter fullscreen mode Exit fullscreen mode

Swagger APIs

We are going to generate our API documentation from code based on comments in the function headers
Back to routes/students.js we are going to add swagger.tags to each of the route. This creates a subsection in our docs page with the tag. (It groups tags with the same name together).
Remember, we are enclosing it in a comment block.

/*  
    #swagger.tags = ['Students']
*/  
Enter fullscreen mode Exit fullscreen mode

Thats what we are adding to each of the routes. Now our students.js routes should look like this:

    const router = require("express").Router();
    const { addOne, removeOne, updateOne, getAll, getOne } = require("../controller/students.js");


    router.get("/students", async (req, res) => {
        /*  
            #swagger.tags = ['Students']
        */ 
        await getAll(req, res);
    })

    //Post 
    router.post('/students', async (req, res) => {
        /*  
            #swagger.tags = ['Students']
        */ 
        await addOne(req, res);
    })

    //Get by ID 
    router.get('/students/get/:id', async (req, res) => {
        /*  
            #swagger.tags = ['Students']
        */  
        await getOne(req, res);
    })

    //Update by ID 
    router.patch('/students/update/:id', async (req, res) => {
        /*  
            #swagger.tags = ['Students']
        */ 
        await updateOne(req, res);
    })

    //Delete by ID 
    router.delete('/students/delete/:id', async (req, res) => {
        /*  
            #swagger.tags = ['Students']
        */ 
        await removeOne(req, res);
    })


    module.exports = router
Enter fullscreen mode Exit fullscreen mode

To be able to see our changes to the route file, we should add a new command to our package.json file.

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon index.js",
    "start-gendoc": "node swagger.js" 
  },
Enter fullscreen mode Exit fullscreen mode

Our scripts in package.json should be like the one above.

The last line will help us generate a json file for swagger that we can view on our browser environment.

Let's go ahead and create a swagger.js file in our root directory.

const swaggerAutogen = require("swagger-autogen")()
require("dotenv").config();

const doc = {
    info: {
        version: "1.0.0",
        title: "Quick start to auto-docs with swagger",
        description: "Auto-documenting our APIs with Swagger documentation. Tutorial by Daniel Olabemiwo"
    },
    host: "localhost:3000", // 3000 is our port
    basePath: "/",
    schemes: ["http", "https"],
    consumes: ["application/json"],
    produces: ["application/json"],
    tags: [
    ],
    definitions: {
        StudentModel: {
            $firstname: "Ade",
            $lastname: "Samuel",
            $middlename: "Daniel",
            $class: "BASIC 3",
            $age: 10
        },
    }
};


const outputFile = "./swagger_output.json";
const endpointFiles = ["./routes/index.js"];

swaggerAutogen(outputFile, endpointFiles, doc).then(() => {
    require("./index");
});
Enter fullscreen mode Exit fullscreen mode

This is how our swagger.js file will look like.

We should also add some swagger lines to our root index.js file.
It should look like this:

require("dotenv").config();
const cors = require("cors");
const express = require("express");

const { connect } = require("mongoose");
const swaggerUi = require("swagger-ui-express");
const swaggerFile = require("./swagger_output.json");

const app = express();
const router = require("./routes/index");

app.use(cors());
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
app.use(router);
app.use("/doc", swaggerUi.serve, swaggerUi.setup(swaggerFile));

const runApp = async () => {
    try {
        await connect(process.env.MONGO_DB, {
            useUnifiedTopology: true,
            useNewUrlParser: true,
        });
        console.log(`Successfully connected to database ${process.env.MONGO_DB}`);
        app.listen(process.env.PORT, () => {
            console.log(`Server started successfully on PORT ${process.env.PORT}`);
        })
    } catch(err) {
        console.log(err);
        runApp();
    }
};

runApp();
Enter fullscreen mode Exit fullscreen mode

Look out for all the new addition to our index.js file.

We should stop our server and run this new command.

npm run start-gendoc
Enter fullscreen mode Exit fullscreen mode

Let's visit our browser and check out http://localhost:3000/doc/

Image description

Congratulations if your browser is looking like the one above!
Now let's wrap up and add necessary parameters to our post request as it requires parameters from users.

    //Post 
    router.post('/students', async (req, res) => {
         /*  
         #swagger.tags = ['Students'],
        #swagger.security = [{
        "Authorization": []
        }]
        #swagger.parameters['obj'] = {
            in: 'body',
            required: true,
            schema: { $ref: "#/definitions/StudentModel" }
    } */ 
        await addOne(req, res);
    })


Enter fullscreen mode Exit fullscreen mode

We have to rerun npm run start-gendoc to reflect all the newest additions.
If you followed this tutorial to this point, you can test the POST endpoint on your browser and should get a result like this:

Image description

This is the end of this tutorial session.

Feel free to drop your comments, or connect with me via twitter or linkedIn

Top comments (0)