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}`)
})
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"
},
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
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();
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
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();
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
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;
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)
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
}
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
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']
*/
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
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"
},
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");
});
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();
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
Let's visit our browser and check out http://localhost:3000/doc/
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);
})
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:
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)