Creating a RESTful Blog API with NodeJS, Express, MongoDB and JWT
Table of contents
- Introduction
- PREREQUISITES
- What is node.js?
- Setting Up Our Server.
- Installing Dependencies
- Creating a .env and CONFIG file and Setting up nodemon
- Connecting to MongoDB
- MVC Architecture
- Creating our Home and User Routes.
- Setting up User authorization/authentication route
- Setting up User Schema and Model
- Updating Our user routes to protect specific routes
- Setting up User/auth Controller
- Blog/article routes
- Setting up Blog/article Schema and Model
- Setting up Blog/article Controller
- Updating Our Entry point app.js
- Handling Logs with Morgan and Winston
- Updating Our Entry point app.js with logs
- Wrap Up
Introduction
This is a follow-up for my second Semester Exam BLOG API project at AltSchool Africa.
You can clone the repo from here to follow up with the tutorial, and you can also view the live App here.
In this article, we are going to build a Blog API with node and Express, where I'll walk you through every step and at the end of this tutorial, you would have learnt:
how to build a fully functional CRUD (
CREATE
,READ
,UPDATE
,DELETE
) application.how to start up a NodeJS application
how to protect your route from unauthorized users (JSON Web Token pack will be used).
log handling with
Joi
validator.how to manage calls to our API with
rate-limit
.how to send and retrieve data from
MongoDB
.how to handle logging with
morgan
andWinston
, and more...
PREREQUISITES
To follow this tutorial more effectively, having a basic understanding of JavaScript & ES6 syntax is required.
Next, you need to have the following setup on your PC
Node
installedvscode
or any code editor of your choiceAny terminal emulator of your choice (Optional)
What is node.js?
Node.js is a JavaScript runtime that allows JavaScript to be used outside of the browser & on the server.
It is primarily deployed for non-blocking, event-driven servers, such as traditional websites and back-end API services.
Now, let's verify that node.js was installed properly. Run this command on your terminal emulator.
node --version
If it returns a version and no error, it means your installation was successful.
Node Application Set up
Create an empty folder
BLOG_API
and on your terminal, navigate to that folder.Enter the command
npm init -y
(NB: this is for NPM. Use the command suitable for the package manager you are using).
npm init -y
This command automatically creates a package manager file.
Now inside the created package.json
file I edited author
, description
and main
(from index.js
to app.js
), you can leave the main
(which is your entry point) as the default index.js
it doesn't matter. Throughout the article, more updates will be made to this file.
Now let's create a file that matches the name app.js
.
touch app.js
Next, we install express
npm i express
At this point your Application folder setup should be like this:
NB: Don't try to edit package-lock.json
and node_modules
.
Setting Up Our Server.
Now, that we have installed express.js, we can set up our server
- In our
app.js
file
- On the terminal
node app.js
Yup! our setup was successful. 👌✔
Installing Dependencies
Before we continue let's install some packages that we will use in this project.
Installing dev dependencies
npm i --save-dev nodemon
Installing our remaining Dependencies
npm i bcrypt dotenv express-rate-limit helmet jsonwebtoken mongoose morgan morgan-json winston
Express.js
: is a minimal and flexible Node.js web application framework.nodemon
: is a command line tool that helps speed up the development of Node.js applications as it monitors and automatically restarts your node application whenever it detects any saved changes.bcrypt
: is Used to hash and salt passwords securely. In simple terms, it is used to encrypt passwords before saving them in the database.dotenv
: It'll be used to load our secret values/keys (environment variables) from a.env
file where we are going to store them.express-rate-limit
: helps to monitor and manage requests made from the same IP address, which helps to prevent attacks like brute force.Helmet
: It is a collection of several smaller middleware functions that set security-related HTTP response headers.jsonwebtoken (JWT)
: Used to add authorization and authentication. We will use this library to add protection to some specific routes.mongoose
: It is an Object Document Mapper (ODM) for MongoDB. We'll use it to connect to our database as well as set up our Schema to model our application, You can read more from the official documentation heremorgan
,morgan-JSON
andWinston
: These libraries will be used to handle logging.
Creating a .env
and CONFIG
file and Setting up nodemon
- Create a .env file in the root of your project folder to save your environment variables.
touch .env
- Create a
Utils
folder, navigate inside the folder and create aconfig.js
file here we'll save the exported environment variables in an object, to be used in any file of our project.
mkdir utils
cd utils
touch config.js
Setting up nodemon
- Go to the
package.json
and update the script section
- Go to your terminal and enter this command to initialize nodemon
npm run dev
Connecting to MongoDB
As indicated at the beginning of this tutorial, we will be using MongoDB
as our preferred database.
MongoDB is a NoSQL cross-platform document-oriented database program.
Before we go ahead to set up and connect our mongoose to the database, let us refactor our code to save the PORT in our .env file and reconnect to our express server.
- go to the
.env
save the port variable.
PORT = 4000
- Next, go to your
config.js
file
require('dotenv').config()
module.exports = {
PORT : process.env.PORT
}
- Then in our
app.js
file
Mongoose connection function set up
Now we know how to set up our environment variables, let's set up our
mongoose
to connect to ourMongoDB
At the root directory of the project create a folder
database
, then navigate inside the folder and create a fileconnectDB
mkdir database
cd database
touch connectDB.js
- In the
connect.js
file set up your mongoose connection
NB: There are various ways of setting up a mongoose connection, feel free to use the method you feel more comfortable with.
Next, We need to get the
URL
of the database we want to connect to, and to do that, we need to signUp/sign in on MongoDB, and complete the process explained here on thisfreecodecamp
article.Just like we did for the
PORT
, save the link to the.env
file and export to theconfig
file.
- We need to set up our database connection, and it's advisable to ensure that the connection to the database is successful before connecting to our server.
One way to achieve this is to create an Async-await function that forces the server to wait for the database to connect first.
- Go to your
app.js
file and create an asynchronous function and call the function.
When nodemon restarts the application.
MVC Architecture
Model-View-controller (MVC) is a software architectural pattern that separates an application into these 3 main logical components.
Model
: This is the central component and it directly manages the data, logic and rules of the application.View
: This is any representation of information.Controller
: Accepts input (usually from users) and converts it to commands for the model or views.
As you already know, this is a Backend Project, so our focus will be on the model and controller
.
It is a nice approach to structure your code from the beginning, so we'll be separating the functions/controller from the routes.
-Let's create 3 directories in the root folder, with 2 files each inside. As you must have guessed correctly, from the names of the files, the articles.js
handles the design of the articles/blogs while the auth/user.js
manages authorized/unauthorized users.
controller
|_articles.js
_authController.js
model
|_articles.js
_user.js
route
|_articles.js
_auth.js
Creating our Home and User Routes.
NB: We will be using “Postman” For our API testing.
Home page
Go to the
app.js
file, enter the details in the image below.Ensure your server is successfully connected.
open
postman
(Or if you are usingthunder client
extension.enter the
http://localhost:4000
and click send.
Setting up User authorization/authentication route
Before we start working on our blog routes, let's create the user routes, where users can:
Sign Up/Register
an AccountSign In/Login
into their registered Account.Update/Edit
their Account.Delete
their registered Account.
- Go to the './route/auth.js' file
const express = require('express');
const authRoute = express.Router()
//authentication route config
const authenticateUser = require('../utils/authMiddelware')
//register a new user
// authRoute.post('/register', registerUser )
//sign in a registered user
authRoute.post('/login', loginUser )
// Update user Account
authRoute.patch('/update/:userID', updateUser )
//Delete user Account
authRoute.delete('/delete/:userID', deleteUser )
module.exports = authRoute;
For a potential user/client to be able to login to our application we have to ensure that the user has been authenticated and even when the user has been authenticated, we have to verify if they are authorized to visit any of the routes they attempt to access.
Authentication *verifies a user's identification from the credentials stored which the user provided when signing up and then uses those credentials to confirm the user's identity.*The authorization process only begins if a user was successfully authenticated, It mostly checks to verify if a user has some specific access.
JSON Web Token (JWT)
We are going to use JWT
as a 'middleware' to implement the authentication/authorization process.
JWT is used to securely transfer information over the web. It consists of 3 parts. A header, payload & signature, and is mostly used for authentication.
(NB: later in this tutorial we'll see how to implement this).
Middleware
Everything is express.js is Middleware.
So middleware functions are very important.
Middlewares are functions that have access to the request
and response
objects and the next
function in the app's request-response cycle. In many cases, the middleware doesn't end the request-response cycle and that is why next()
is important as it passes the control to the next function.
NB: failure to call the next() in this scenario, will leave the application hanging.
- The syntax for middleware is
app.get(path, `(req, res, next) => {}`, (req, res) => {})
Setting up User Schema and Model
Go to "./model/user.js file to create the user schema.
We require/import the mongoose library.
JWT
as stated earlier will be used for authentication,
JWT_SECRET
andJWT_LIFETIME
are environment variables, JWT_SECRET can be a set of alpha-numeric characters we generatee.g xcfdsaqwergege23xdfegege233
while for JWT_LIFETIME we will be setting the expiration of the token to1h
(1 hour).
bcrypt will be used to encrypt the user's password before it is saved to the database. (note: it is bad practice to save the user's password in plain text).const mongoose = require('mongoose') const jwt = require('jsonwebtoken') const bcrypt = require('bcrypt') const Schema = mongoose.Schema const { JWT_SECRET, JWT_LIFETIME } = require('../utils/config')
Next, we build our schema
const ArticleSchema = new Schema({ title:{ type : String, required: [true, "Please include a title to your article"], unique : [true, "title already exists, please try another title"], lowercase : true }, description :{type : String }, author: { type : mongoose.Schema.Types.ObjectId, ref : 'User', required : true, }, content : { type: String, required: [true, " No content added"] }, tags : { type : String, default: "Others", lowercase : true }, state :{ type : String, enum : ["draft", "published"], default : "draft", lowercase : true }, read_count : { type : Number, default :0 }, reading_time: { type: String } }, { timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' }} ) const ArticleModel = mongoose.model('Article', ArticleSchema); module.exports = ArticleModel
Next, we add the pre-hook mongoose middle function save
, to enable encrypting the password, before saving it to the database.
//pre-hook function to automatically hash users password before its saved to the database
UserSchema.pre('save', async function(next) {
const salt = await bcrypt.genSalt(10);
const hashpassed = await bcrypt.hash(this.password, salt);
this.password = hashpassed;
next()
})
Next, we'll create 2 Mongoose methods
functions.
The first, I named
generateJWT,
will generate a '1 hour' valid token whenever a user successfully registers or logs into the application.The second
isValidPassword
will be called when a user tries to log in their password. It uses bcryptcompare
method, to compare the encrypted password with the password the user is signing in with to see if it matches.
//mongoose schema method to generate a token when a user registers or logs in successfully
UserSchema.methods.generateJWT = function () {
return jwt.sign({userID : this._id, email : this.email, first_name : this.first_name, last_name : this.last_name },
JWT_SECRET,
{
expiresIn : JWT_LIFETIME || '1h'
}
);
}
//this method when called,it ensures that users trying to login, enter their correct passowrd,...
//...so this function is necessary since the password in the database in encrypted
UserSchema.methods.isValidPassword = async function(password ){
const compare = await bcrypt.compare(password, this.password);
return compare
}
Next, we create the mongoose model, with 2 arguments, the first will be the name of the collection on the MongoDB databaseNOTE: MongoDB will automatically pluralize the collection name. So 'User' will be saved as 'users'.
Then, we export
const UserModel = mongoose.model('User', UserSchema);
module.exports = UserModel;
Setting up the JSON web token (JWT) authentication middleware
Remember in our user "./model/user.js file, we set up JWT to create used the user's payload and secret key to create a token, whenever they sign up or sign in to the application.
Now, we also use JWT to set up our authentication middleware, which will be capable of verifying the user from the token provided.
Go to the utils folder and create a new file './utils/authMiddleware.js
```javascript const jwt = require('jsonwebtoken') const { JWT_SECRET } = require('./config')
const authenticate = (req, res, next) => {
const authHeader = req.headers.authorization;
if(!authHeader || ! authHeader.startsWith('Bearer ')){
throw new Error('Invalid Authentication')
}
const token = authHeader.split(" ")[1];
try {
const payload = jwt.verify(token, JWT_SECRET)
//useful when attaching the user[author] to the protected articles route
req.user = {userID : payload.userID, email : payload.email, first_name : payload.first_name, last_name : payload.last_name}
next()
} catch (error) {
throw new Error('Invalid Authentication')
}
}
module.exports = authenticate
```
NB: One additional advantage of this authenticate
function set-up is that when it is used as a middleware in any of our routes, then from that route, we'll have access to the details of the signed-in user. This will be very important later in this tutorial when we want to automatically
save articles with the name of their creators by 'referencing' from the user model
.
Updating Our user routes to protect specific routes
Go to the './route/auth.js file'
```javascript const express = require('express'); //authentication route config const authenticateUser = require('../utils/authMiddelware') const authRoute = express.Router() const {
registerUser, loginUser, updateUser, deleteUser, } = require('../controller/authController');
//register a new user
authRoute.post('/register', registerUser )
//sign in a registered user
authRoute.post('/login', loginUser )
//User edits their account
authRoute.patch('/update/:userID', authenticateUser, updateUser )
//user deletes account
authRoute.delete('/delete/:userID',authenticateUser, deleteUser )
module.exports = authRoute;
```
Setting up User/auth Controller
Now, we have our user routes and model set up and JSON web token set up, let's set up the controller functions.
- Go to the './controller/authController' route
Firstly we import/require the User Model we just created
const UserModel = require('../models/User');
Now let's create the controller functions for our 4 routes
const UserModel = require('../models/User');
//sign up ~ register
const registerUser = async(req, res) =>{
const {first_name, last_name, email, password} = req.body;
if(!first_name|| !last_name || !email || !password){
return res.status(400).json({ message : `Please Enter all required fields`})
}
try {
//check if the potential new User's email already exist
const emailAlreadyExist = await UserModel.findOne({ email });
if(emailAlreadyExist){
return res.status(401).json({msg : `This email '${email}' already exist... try a different email`})
}
const user = new UserModel({
first_name : first_name,
last_name : last_name,
email : email,
password : password //nb: mongoose userSchema pre-hook already handled the password encryption
})
const token = user.generateJWT() //called this function from the userSchma mongoose method
await user.save()
res.status(201).json({
success : true,
message : 'Registration Completed!!!',
user:{
id : user._id,
first_name : user.first_name,
last_name : user.last_name,
token
}
})
} catch (error) {
res.status(500).json({ message : error.message })
}
}
//signin ~ login
const loginUser = async(req, res) =>{
try {
const { email, password} = req.body;
if(!email || !password){
return res.status(400).json({ message : `Please provide your email and password`})
}
const user = await UserModel.findOne({ email})
if(!user){
return res.status(401).json({message : `Invalid Credentials`})
}
const validate = await user.isValidPassword(password);
if(!validate){
return res.status(401).json({message: `Incorrect password! Please try again`})
}
const token = user.generateJWT() //called this function from the userSchma mongoose method
return res.status(200).json({
success : true,
message : 'logged in successfully',
user : {
id : user.id,
token
}
})
} catch (error) {
res.status(500).json({ message : error.message })
}
}
//function for users to edit/update their user Account
const updateUser = async(req , res) =>{
//grabs the user's id from token
const userID = req.user.userID;
const { first_name, last_name, email, password } = req.body;
try {
if(userID !== req.params.userID){
return res.status(401).json({messsage : 'Unauthorized'})
}
const user = await UserModel.findOne( { _id: userID })
if(!user ) {
return res.status(401).json({messsage : 'Unauthorized'})
}
user.first_name = first_name
user.last_name = last_name
user.email = email
user.password = password
await user.save()
return res.status(200).json(
{success : true,
message : `Your account has been updated successfully`,
user: {
first_name : user.first_name,
last_name : user.last_name,
userID : user._id,
email : user.email,
password : user.password
}
})
} catch (error) {
res.status(500).json({status : false, message : error.message})
}
}
//Delete user account
const deleteUser = async (req, res) => {
//grabs the user's id from token
const userID = req.user.userID;
try {
if(userID !== req.params.userID){
return res.status(401).json({messsage : 'Unauthorized'})
}
const user = await UserModel.findOne({ _id : userID})
if(!user) {
return res.status(401).json({messsage : 'Unauthorized'})
}
await user.delete()
return res.status(200).json({ success : true, message: `Your account has been successfully deleted`})
} catch (error) {
res.status(500).json({status : false, message : error.message})
}
}
Lastly, we export the controller functions with
module.exports
to make it possible for us to require and use in ourroute
section.module.exports = { registerUser, loginUser, updateUser, deleteUser }
Blog/article routes
These are the article routes we will be working on, and if you noticed it covered the CRUD
operation
CREATE -
POST
methodREAD -
GET
methodUPDATE -
PATCH
methodDELETE -
DELETE
methodGo to the './route/articles.js' file and create your unprotected and protected routes.
```javascript const express =require('express');
//authentication route config const authenticateUser = require('../utils/authMiddelware') //calling the router function const router = express.Router()
//importing the CRUD functions from the controller folder const {
createArticle, getMyArticles, getAllArticles, getArticle, updateArticle, deleteArticle
} =require('../controller/articles')
//create a new blog post router.post('/new', authenticateUser, createArticle)
//get all the published aritcles router.get('/', getAllArticles);
//Requires authentication to enable the Owner get a list of their articles router.get('/my-articles', authenticateUser, getMyArticles)
//get a published aritcle id : {blogID} router.get('/:blogID', getArticle);
//Edit/Updating an article router.patch('/:blogID', authenticateUser, updateArticle)
//delete an article router.delete('/:blogID', authenticateUser, deleteArticle)
module.exports = router
```
Setting up Blog/article Schema and Model
Before we set up the controllers to enable us to make our routes functional, we have to create a mongoose Schema
. This will determine how our data will be stored in our database.
Go to the './model/articles.js
Similarly to the user model, we require the mongoose library and set up our
Schema
and then we export.
const mongoose =require('mongoose')
const ArticleSchema = new mongoose.Schema({
title:{
type : String,
required: [true, "Please include a title to your article"],
unique : [true, "title already exists, please try another title"],
lowercase : true
},
description : {type : String },
author: {
type : mongoose.Schema.Types.ObjectId,
ref : 'User',
required : true,
},
content : {
type: String,
required: [true, " No content added"]
},
tags : {
type : String,
default: "Others",
lowercase : true
},
state :{
type : String,
enum : ["draft", "published"],
default : "draft",
lowercase : true
},
read_count : {
type : Number,
default :0
},
reading_time: {
type: String
}
}, { timestamps: {
createdAt: 'created_at',
updatedAt: 'updated_at'
}
})
const ArticleModel = mongoose.model('Article', ArticleSchema);
module.exports = ArticleModel
In the author
field, we will notice a ref
option having a value 'User'
. This ref option tells Mongoose which model to use during population, in our case, it is the 'User' model, it also has a special mongoose type .ObjectId
that store id from the 'User'
model.
In the reading_time
field, we are to come up with an algorithm. We'll create a function that will help us calculate the reading_time of each article by calculating the words. This will be done in our POST route for creating a new post router.post('/new', authenticateUser, createArticle)
In the read_count
field, we'll calculate the number of times an individual article/blog post has been read, by incrementing it by 1, anything that article is searched. This will be done in our GET route to search for an article router.get('/:blogID', getArticle)
Setting up Blog/article Controller
Now let's set up Our blog/article controller
- Go to the './controller/articles.js' file
const ArticleModel = require("../models/articles");
//-----------------function to create an article on the blog site-----------------
const createArticle = async (req, res) =>{
//grabs the user's id from the token in the JWT authorization middleware
req.body.author = req.user.userID;
// creating a function to calculate the reading time
const calcReadingTime = (text) => {
const wordPerMinute = 200 //assumes that it takes 200 words to read/min (ref: google)
const numOfWords = text.split(/\s/g).length //uses regex to count words by white-spaces, newlines
const mins = (numOfWords/ wordPerMinute) * 1
const readTime = Math.ceil(mins) //aprox. to the next whole Number
return `${readTime} minute(s) read`
}
try {
const newArticle = await new ArticleModel({
title : req.body.title,
description : req.body.description,
author : req.body.author,
content: req.body.content,
tags : req.body.tags,
read_count: req.body.read_count,
reading_time : calcReadingTime(req.body.content),
})
const article = await newArticle.save()
return res.status(201).json({status : true, article });
} catch (error) {
res.status(500).json({status : false, message : error.message})
}
}
//------------displaying all the published articles on the blog---------------------
const getAllArticles= async(req, res) =>{
try {
const { author, title, tags, sort } = req.query;
const findQuery = {
state : "published"
//This 'hard coded line ensures on 'published' are displayed on this route
};
//Q-14b. --searchable(filter) by the author, Blog's title & tags
if( author ){
findQuery.author = author
}
if( title ){
findQuery.title = { $regex : title, $options : 'i' }
//using regex : enables us to search for titles even with the incomplete word [e.g 'ok' ==> 'look' & 'okay'];
//adding the 'i' options : is to make our search words to be 'case Insensitive'
}
if( tags ){
findQuery.tags = tags
}
let result = ArticleModel.find(findQuery)
// ..it should be orderable .......
if(sort){
const sortListItems = sort.split(',').join(' ');
result = result.sort(sortListItems)
//this dynamically sorts the list with different available items
}else{
result = result.sort('-read_count ,reading_time, -created_at')
//by default, this sorts by the read_count..
// [highest-lowest] --> reading_time[shortest - longest] --> created_at[newest - oldest]
}
// paginate (default 20 blogs per page)
const page = parseInt(req.query.page) || 1
const limit = parseInt(req.query.limit) || 20
const skip = (page - 1) * limit
//this skip logic ensures that the results starts from the actual first item,
//''' it also ensures that the next page starts from the next item and not the last item of the previous result
result = result
.skip(skip)
.limit(limit)
.populate({
path : 'author',
select :{
first_name : 1,
last_name : 2
}
})
const article = await result
return res.status(200).json({ status : true, article, count : article.length})
} catch (error) {
res.status(500).json({status : false, message : error.message})
}
}
//--------------displaying Onwer's Articles--------------------
const getMyArticles = async( req, res) =>{
//Owners blogs should be paginated
const page = Number(req.query.page) || 1 //page 1:default
const limit = Number(req.query.limit) || 10 //10 per page
const skip = (page - 1) * limit
try {
const article = await ArticleModel
.find({author : req.user.userID})
.sort('-state')
.skip(skip)
.limit(limit)
return res.status(200).json({status : true, article, count: article.length})
} catch (error) {
res.status(500).json({status : false, message : error.message})
}
}
//----------------search and GET an article--------------------
const getArticle = async(req, res) =>{
const {blogID} = req.params;
const article = await ArticleModel.findOne({ _id : blogID})
.populate({
path : 'author',
select :{
first_name : 1,
last_name : 2,
email : 3
}
//Q-15a. I used 'populate' here to generate additional information about the author
})
if(!article){
return res.status(404).json({status : false, message: `No article with the ID: ${blogID} found!`})
}
//Q-15b. increments a blog post by 1 everytime it is read/requested
article.read_count ++
try {
await article.save()
return res.status(200).json({success : true , article })
} catch (error) {
res.status(500).json({status : false, message : error.message})
}
}
//Editing or Updating an article
const updateArticle = async(req, res) =>{
//grabs the user's id from token
req.body.author = req.user.userID;
const { blogID } = req.params;
const { body }= req;
if(body ===" "){
throw new Error('fields cannot be left empty')
}
try {
const article = await ArticleModel.findOneAndUpdate({_id: blogID, author : req.user.userID}, body,{
new: true,
runValidators:true
});
if(!article){
return res.status(401).json({status : false, msg: `You're NOT authorized to Edit this article ID:${blogID}`})
}
res.status(200).json({success : true, article })
} catch (error) {
res.status(500).json({status : false, message : error.message})
}
}
//Deleting an article
const deleteArticle = async(req, res)=>{
//grabs the user's id from token
req.body.author = req.user.userID;
const {blogID} = req.params;
try {
const article = await ArticleModel.findOneAndRemove({_id: blogID, author: req.body.author})
if(!article){
return res.status(401).json({status : false, msg: `You're Unauthorized to delete this article ID:${blogID}`})
}
res.status(200).json({success : true, message :`Successful! Your Article has been Deleted!`})
} catch (error) {
res.status(500).json({status : false, message : error.message})
}
}
module.exports={
createArticle,
getArticle,
getAllArticles,
getMyArticles,
updateArticle,
deleteArticle,
}
Apart from creating our Article routes and successfully implementing the logic we talked about earlier on, you'll notice we added some other features like pagination
and we also used the .populate
property to add more details about the authors to their blogs.
Updating Our Entry point app.js
router middlewares
helmet (adds security)
express-rate-limit
express JSON and URL encoded (enables our application to have access to user's input)
invalid '404' handler (manages any undefined route)
const express = require('express')
const rateLimit = require('express-rate-limit')
const helmet = require('helmet')
//importing the mongoDB dataBase configuration file
const connectDB = require('./database/connectDB')
const app = express();
const { PORT, MONGO_DB_URI } = require('./utils/config')
//route for register & signIn
const authUser = require('./route/auth')
//blog route config
const articleRouter = require('./route/articlesRoute');
//Security
app.use(helmet())
const limiter = rateLimit({
windowMs: 10 * 60 * 1000, // 10 minute
max: 80, // Limit each IP to 80 requests per `window` (here, per 10 minutes)
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
})
// Apply the rate limiting middleware to all requests
app.use(limiter)
//middleware to grab post/patch requests as json files or other files
app.use(express.json())
app.use(express.urlencoded({extended : true}))
//home page
app.get('/',(req, res) =>{
res.send({
success : true,
message : "Welcome to My Blog HomePage ",
"Article route" : "/api/v1/articles"
})
})
//app middleware to the user registration & login routes
app.use('/api/v1/auth', authUser )
//app middleware to the blog article route
app.use('/api/v1/articles', articleRouter)
//handling invalid (404) routes
app.all('*', (req, res) =>{
res.status(404).json({ message : "This route does not exist"})
})
//setting up the Database and server connections
const start = async() =>{
try{
await connectDB(MONGO_DB_URI)
console.log('database connected')
app.listen( PORT, () => {console.log(`Server listening at port: ${PORT}`) })
}catch (error) {
console.log('Unable to connect to the Database ' + error)
}
}
//starts the connections
start();
Handling Logs with Morgan and Winston
Application logging is a critical part of log management and can help keep your business running smoothly and securely.
Application logging is the process of saving application events. With this information in hand, you can assess threats and analyze errors before they disrupt broader business workflows.
We're going to combine Winston
and morgan
(with the help of another library Winston-JSON
) to handle our logging.
Setting up Winston
Create a new folder
logger
in the root folder, navigate into it and create a filelogger.js.
mkdir logger cd logger touch logger.js
next, we require the Winston library
Winston is a multi-transport async logging library for Node.js. A transport is essentially a storage device for your logs.
What this means is that with Winston you can save different levels (or all logs ) at different locations.
Note that
npm
logging levels are prioritized from 0 to 6 (highest to lowest) and if you specify a level to be logged, it logs that level and all levels below (for example. if you set log for HTTP, it will logHTTP
,info
,warn
anderror
levels)In this tutorial, we will set our
'info'
logs to save to a file'./log/app.log'
while setting our'debug'
logs to display on the console.
Now, let us require/import Winston
const winston = require('winston')
Next, let's create our Winston transport
const options = {
file : {
level : 'info',
filename : './log/app.log',
handleExceptions : true,
json : true,
maxsize : 5242880, //5MB
maxFile : 5,
colorize : false
},
console : {
level: 'debug',
handleExceptions : true,
json : false,
colorize : true
}
}
const logger = winston.createLogger({
levels : winston.config.npm.levels,
transports : [
new winston.transports.File(options.file),
new winston.transports.Console(options.console)
],
exitOnError : false
})
module.exports = logger
- Next, in the
logger
folder let's create a second filehttpLogger.js
We require the morgan
and morgan-json
library and also require the logger that was created with Winston.
const morgan = require('morgan')
const json =require('morgan-json')
const logger = require('./logger')
Next, we defined the format using morgan-json
const format = json({
method :':method',
url : ':url' ,
status : ':status',
contentLength: ':res[content-length]',
responseTime: ':response-time ms'
});
Using Winston in morgan
const httpLogger= morgan(format,{
stream: {
write: (message) => {
// winston.info(message);
const { method, url, status, contentLength, responseTime } = JSON.parse(message)
logger.info('HTTP Access log', {
timestamp: new Date().toString(),
method,
url,
status: Number(status),
contentLength,
responseTime:Number(responseTime)
})
}
}
})
module.exports = httpLogger
Updating Our Entry point app.js
with logs
- we will import the
httpLogger
( morgan-Winston ) file and use it as middleware.
we will import the logger ( Winston ) file, and use it to update our console logs
console.log --> logger.info, logger.error
Our Updated app.js
file
const express = require('express')
const rateLimit = require('express-rate-limit')
const helmet = require('helmet')
//importing the mongoDB dataBase configuration file
const httpLogger = require('./logger/httpLogger')
//winston logger
const logger = require('./logger/logger')
const connectDB = require('./database/connectDB')
const { PORT, MONGO_DB_URI } = require('./utils/config')
const app = express();
//route for register & signIn
const authUser = require('./route/auth')
//blog route config
const articleRouter = require('./route/articlesRoute');
//Security
app.use(helmet())
//morgan/winston logger
app.use(httpLogger)
const limiter = rateLimit({
windowMs: 10 * 60 * 1000, // 10 minute
max: 80, // Limit each IP to 80 requests per `window` (here, per 10 minutes)
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
})
// Apply the rate limiting middleware to all requests
app.use(limiter)
//middleware to grab post/patch requests as json files or other files
app.use(express.json())
app.use(express.urlencoded({extended : true}))
//home page
app.get('/',(req, res) =>{
res.send({
success : true,
message : "Welcome to My Blog HomePage ",
"Article route" : "/api/v1/articles"
})
})
//app middleware to the user registration & login routes
app.use('/api/v1/auth', authUser )
//app middleware to the blog article route
app.use('/api/v1/articles', articleRouter)
//handling invalid (404) routes
app.all('*', (req, res) =>{
res.status(404).json({ message : "This route does not exist"})
})
//setting up the Database and server connections
const start = async() =>{
try{
await connectDB(MONGO_DB_URI)
logger.info('database connected')
app.listen( PORT, () => { logger.info(`Server listening at port: ${PORT}`) })
}catch (error) {
logger.error('Unable to connect to the Database ' + error)
}
}
//starts the connections
start();
Now, when we reconnect to our servers, we will notice that the logging level alongside the display message is displayed in a JSON format. We'll also notice that a file with our log file
'./log/app.log'
was automatically created because of our configuration in the Winston file.Now let's visit an invalid route
http://localhost:4000/articles
and then our valid blog route that displays all the published articleshttp://localhost:4000/api/v1/articles
We will notice more information in our log, (like the express method, status code, date-time e.t.c) as defined with our
morgan-json
andmorgan
set up.
Wrap Up
We have come to the end of this tutorial, and we have been able to build an excellent Node.js Blog API, where Users can Sign up, Create and publish their own blog posts/articles, Edit and delete their articles, as well as Update and delete their account details.
I hope you learnt a lot from this tutorial as well as the links attached for a deeper understanding. In case you missed it, Here's another link to the repo on GitHub.