Building a blog Api with Nodejs
Tags: node, javascript
Published: 2023-01-10 04:33:54

Hello everyone!.In this article, we would building a blog API with the certain requirements.

|  Method  |  Endpoint | Description  |
|------------|----------|-----------|
| POST        | /api/v1/users/signup      | Creates a new user       |
| GET      | /api/v1/users/signup    | login a user     |
| POST      | /api/v1/articles    | Creates a new article     |
| GET     | /api/v1/articles  | get all articles    |
| GET      | /api/v1/users/articles    | Get all articles     |
| GET      | /api/v1/articles/userblog    | Get all the articles of a user     |
| PATCH      | /api/v1/users/articleId    | User edit an article     |
| GET      | /api/v1/users/articleId   | User get an article by id     |
| DELETE      | /api/v1/users/articleId    | User delete an article     |




 


## Before we start..

.
Some prerequisites you need to follow along:

- Beginner to intermediate knowledge in JavaScript
- Basic understanding of what REST APIs are
- Basic understanding of back-end programming and Node.js

 **Some tools we will use:**
Visual Studio Code or any text editor

- POSTman
- Node.js and express
- MongoDB Atlas




## Setup


Let's get started with the Node.js code. Make a folder called Blog API.

```
mkdir blog-api
```
Navigate to the folder

```
cd blog-api
```
Initialize the project

```
yarn init -y
```
We would install some packages before proceeding.
These packages will be used in the the project.
Install Express,Mongoose,dotenv,bcryptjs using the terminal.

```
yarn add express mongoose dotenv bcryptjs
```

Installing nodemon as dev dependency,nodemon would help us automatically restart our server anytime a change is made.

```
yarn add nodemon -D
```
When the packages are installed, make a file called .env.

```
touch .env
```
Go to [mongodb website](https://www.mongodb.com/atlas/database) and create a mongodb cluster and paste the connections string in your env file.

```
mongodb+srv://user:password@cluster0.hex8l.mongodb.net/name-database?retryWrites=true&w=majority
```
We'll make use of the dotenv package to connect it to the apllication. The package assists us in loading enviroment varaibles into our codes. This is helpful when uploading your project to GitHub. where you may not want your database login details.

let set up the application..we create a file named app.js

```
touch app.js
```
This file would be the entry point of our apllication

## app.js

```
const express = require("express");

const PORT = 8000;

const app = express();

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

module.exports = app;
```

so now let us create a config folder where i would place the database connection file.

```
mkdir config
touch db.js
```
In our db.js file i would connect our application to mongodb

## db.js

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

const Dbconnect = async() => {
    try {
        const dbconn = await mongoose.connect(process.env.MONGO_URI);
    console.log(`db connected at ${dbconn.connection.host}`);    
    } catch (error) {
        console.log(error);
        process.exit(1);
    }
}

module.exports = {
    Dbconnect,
}
```
we need to create a server.js file


## server.js

```
const { Dbconnect } = require('./config/db')
const app = require('./app');
const dotenv = require('dotenv');
dotenv.config();


const port = process.env.PORT;

Dbconnect();

app.listen(port, () => {
  console.log(`port running on port ${port}`);
});
```

Creating the Model for the apllication
Now that we are done with the basic setup of the application,we will have to create the models for our appilication .You know we are creating a blog application so we would have two models one for the user and the other for article. 

## user model(usermodel.js)
```
const mongoose = require('mongoose');

const userSchema = mongoose.Schema(
  {
    firstName: { type: String, required: true },
    lastName: { type: String, required: true },
    email: { type: String, required: true },
    password: { type: String, required: true },
  },
  {
    timestamps: true,
  }
);

module.exports = mongoose.model('User', userSchema);
```
we exported our code in order to be able to use it in other parts of our application.he schema contains details such as username, email, and password that will be stored in the database.
Let us now create the article Schema in which we will store what we want to be in our article.


## article model(articlemodel.js)

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


const articleSchema = mongoose.Schema({
  title: { type: String, required: true ,unique:true},
  description: { type: String, required: true },
  body: { type: String, required: true },
  author: { type: String, required: true },
  user_id: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
  state: { type: String, default: 'draft', enum: ['draft', 'published'] },
  readCount: { type: Number, default: 0 },
  readingTime: { type: String },
  tags: [String],
}, {
  timestamps: true,
})


module.exports = mongoose.model('Article', articleSchema);
```

In the article schema,each article is suppose to have the following:

- title - This is the title of the article

- decription - This is just a basic description of what the article is all about

- body - This is the actual content of the article

- author - This is the user who wrote/published the article

- user_id - This is the id of the user who published the article

- state - This is the state of the article each article has a default state of draft when created.the author can change the state to published

- readcount - This is the number of times an article has been read

- readingtime - This is the time i takes to read the content of the article

- tag - This provides a way to group related articles together

- timestamps - Each article has a creation date .





 

## Lets create our controller ,middleware and router 



We would create an helper class that would contain all our helper functions.the file would be in our utils folder

```
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');

class Helper {
  
  static calculateReadingTime(body) {
    const readtime = Math.round(body.split(' ').length / 200);
    const readingTime =
      readtime < 1 ? `${readtime + 1} mins read` : `${readtime} mins read`;
    return readingTime;
  }

  static async  hashPassword(password)  {
    const salt = await bcrypt.genSalt(10);
    const hashedPassword = await bcrypt.hash(password, salt);
    return hashedPassword;
  }
  
  static async comparePassword(password, hashedPassword) {
    const userpassword = await bcrypt.compare(password, hashedPassword);
    return userpassword;
  }

  static async generateToken(payload) {
    const token = await jwt.sign(payload, process.env.SECRET, {
      expiresIn: '1h',
    });
    return token;
  }
  
  static async decodeToken(token) {
    const payload = await jwt.verify(token, process.env.SECRET);
    return payload;
  }

  
}



module.exports = {
  Helper
};

```
In the helper class above the the functions in the class does the following:

- The reading time function accepts the body of the article as a parameter and calculates thye time it would take to read the article.

- The hashpassword function takes the user password and hashes it and returns the hashed password.

- The comparepassword function takes the both the user password and the hashed password and compare both if they are the same.

- The generatetoken function it takes the payload which is the user information and generate a token from the user details and the token is also set to expire after an hour.

- The decodetoken function takes the doken and decrypts it to get the user details in it.

we would  also create an response file in our utils folder this file would design our app response.



##  response.js


```
function errorResponse(res, statusCode, error) {
  const resObj = { error, statusCode };
  return res.status(statusCode).send(resObj);
}

function successResponse(res, statusCode, message, data = []) {
  const resObj = { message, statusCode, data };
  return res.status(statusCode).send(resObj);
}

function handleError(err, req) {
  console.log(
    `error message:${JSON.stringify(err.message)},
    Error caught at: ${JSON.stringify(req.path)}
    `
  );
}



module.exports = {
  errorResponse,
  successResponse,
  handleError,
};

```
 

We are going to create a usercontroller file in the controller folder.this file would handle signing up of users and logging in a user.

## user controller(usercontroller.js)
```
const User = require('../model/usermodel');
const { Helper } = require('../utils/genutils');
const {
  errorResponse,
  successResponse,
  handleError,
} = require('../utils/responses');

const signUp = async (req, res) => {
  try {
    const { firstName, lastName, email, password } = req.body;
    if (!firstName || !lastName || !email || !password) {
      return errorResponse(res, 401, 'incomplete credentials');
    }
    const userExist = await User.findOne({ email });
    if (userExist) return errorResponse(res, 401, 'user already exist');
    const hash = await Helper.hashPassword(password);
    const user = await User.create({
      firstName,
      lastName,
      email,
      password: hash,
    });
    return successResponse(res, 201, 'user created successfully', user);
  } catch (error) {
    handleError(error, req);
    return errorResponse(res, 500, 'Server error');
  }
};

const login = async (req, res) => {
  try {
    const { email, password } = req.body;
    if (!email || !password) {
      return errorResponse(res, 401, 'incomplete credentials');
    }
    const user = await User.findOne({ email });
    if (!user) return errorResponse(res, 404, 'user not found');
    const isPassword = await Helper.comparePassword(password, user.password);
    if (!isPassword) return errorResponse(res, 401, 'incorrect password');
    const token = await Helper.generateToken({
      id: user.id,
      firstName: user.firstName,
      lastName: user.lastName,
    });
    return successResponse(res, 200, 'user logged in successfully', {
      user,
      token,
    });
  } catch (error) {
    handleError(error, req);
    return errorResponse(res, 500, 'Server error');
  }
};

module.exports = {
  signUp,
  login,
};

```

- In the signup function we accept the user details we check if it complete and if it's not we return an error response .we also check the email to check if the user already exist using the `findOne` method.if the user exist we return an error response saying 'the user already exists; but if it doesn't we hash the user password and we create the user in the database.

- In the login function we take the email and password from the user,we check if there is a user with that email in our database using the `findOne` method. if we don't find any user with that database we return error saying 'user does not exist '.if we find a user we compare the user password with the hashed password and if it doesn't match we give an error response that the 'user password does not match' if it matches we generate a token.and send back a success response
 





## User Route

Lets create the user routes to be able to test our signup and login codes

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

const userRouter = express.Router();


const { signUp,login } = require('../controllers/usercontroller')


userRouter.route('/signup').post(signUp);
userRouter.route('/login').get(login);

module.exports = userRouter;
```
So lets now update our app.js file with the userroute file

```
const dotenv = require('dotenv');
dotenv.config();

const express = require('express');

const userRouter = require('./routes/userroute');

const app = express();

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

app.get('/',(req,res) => {
  res.send('welcome to my blog application');
})

app.use('/api/v1/users', userRouter);

module.exports = app;

```
Once you are done with updating your app.js file,we would have to start the server and test the endpoints using the command

```
nodemon server
```
Before we  create our article controller we would have to create a user middleware that would help in authenticating a user and that some particular details truly belongs to them.

So we create auth.js in the middleware folder

## auth.js

```
const User = require('../model/usermodel');
const { Helper } = require('../utils/genutils');
const { errorResponse } = require('../utils/responses');

const authguard = async (req, res, next) => {
  try {
    if (req.headers && req.headers.authorization) {
      const token = await req.headers.authorization.split(' ')[1];
      const decode = await Helper.decodeToken(token);
      const user = await User.findById(decode.id);
      if (!user) return errorResponse(res, 401, 'user not found');
      req.user = user;
      return next();
    } else {
      return errorResponse(res, 401, 'authorization not found');
    }
  } catch (error) {
    return errorResponse(res, 500, error.message);
  }
};

module.exports = {
  authguard,
};

```
In this file we extract the user token from the header and then we decode the user token to get the user information from the token the we check if there is  actually a user with the id gotten from the token.then we store it in the req.user .

Now that we are done we our middleware,let's now move to creating our article controller. we would create an article controller in our controller folder

## article controller

```
const Article = require('../model/articlemodel');
const { Helper } = require('../utils/genutils');
const { successResponse, handleError, errorResponse } = require('../utils/responses');

const createArticle = async (req, res) => {
  try {
    const { title, description, body, tags } = req.body;
    const { id } = req.user;
    const author = `${req.user.firstName} ${req.user.lastName}`;
    const readingTime = Helper.calculateReadingTime(body);
    const article = await Article.create({
      title,
      description,
      body,
      tags,
      author,
      user_id: id,
      readingTime: readingTime,
    });
    return successResponse(res, 201, 'article published successfully', { article });
  } catch (error) {
    handleError(error, req);
    return errorResponse(res, 500, 'Server error');
  }
};

const getAllArticles = async (req, res) => {
  try {
    const page = req.query.page || 1
    const limit = req.query.limit || 20
    const skip = (page - 1) * 10;
    const search = {};
    if (req.query.author) {
      search.author = req.query.author
    } else if (req.query.title) {
      search.title = req.query.title
    } else if (req.query.tags) {
      search.tags = req.query.tags
    }
    const articles = await Article.find(search).sort({readCount: 1,readingTime: -1,createdAt: -1}).skip(skip).limit(limit).where({ state: 'published' });
    if (!articles) return errorResponse(res, 404, 'No articles found');
    return successResponse(res, 200, 'post fetched successfully', { articleNumbers: articles.length, page: page, articles: articles })
  } catch (error) {
    handleError(error, req);
    return errorResponse(res, 500, 'Server error');
  }
}

const getAllArticleById = async (req, res) => {
  try {
    const { articleId } = req.params;
    const article = await Article.findById(articleId).populate('user_id', { firstName: 1, lastName: 1 })
    if (!article) return errorResponse(res, 404, 'Article not found');
    article.readCount += 1
    const result = await article.save();
    return successResponse(res, 200, 'article fetched successfully', result);
  } catch (error) {
    handleError(error, req);
    return errorResponse(res, 500, 'Server error');
  }
}

const editArticle = async (req, res) => {
  try {
    const { id } = req.user;
    const { articleId } = req.params;
    const { body,title,description,tags } = req.body;
    const article = await Article.findById(articleId);
    if (!article) return errorResponse(res, 404, 'Article not found');
    if (article.user_id.toString() != id) return errorResponse(res, 401, 'user not authorized');
    const editedArticle = await Article.findByIdAndUpdate(article, { body,title,description,tags }, { new: true });
    return successResponse(res, 200, 'article updated successfully', editedArticle);
  } catch (error) {
    handleError(error, req);
    return errorResponse(res, 500, 'Server error');
  }
}

const updateArticle = async (req, res) => {
  try {
    const { id } = req.user;
    const { articleId } = req.params;
    const article = await Article.findById(articleId);
    if (!article) return errorResponse(res, 404, 'Article not found');
    if (article.user_id.toString() != id) return errorResponse(res, 401, 'user not authorized');
    article.state = 'published';
    const result = await article.save();
    return successResponse(res, 200, 'article updated successfully', result);
  } catch (error) {
    handleError(error, req);
    return errorResponse(res, 500, 'Server error');
  }
}

const deleteArticle = async (req, res) => {
  try {
    const { id } = req.user;
    const { articleId } = req.params;
    const article = await Article.findById(articleId);
    if (!article) return errorResponse(res, 404, 'Article not found');
    if (article.user_id.toString() != id) return errorResponse(res, 401, 'user not authorized');
    const deletedArticle = await Article.findByIdAndDelete(article);
    return successResponse(res, 200, 'article deleted successfully', articleId);
  } catch (error) {
    handleError(error, req);
    return errorResponse(res, 500, 'Server error');
  }
}

const getAllUserArticle = async (req, res) => {
  try {
    const { id } = req.user;
    const { state } = req.query
    const page = req.query.page || 1;
    const limit = req.query.limit || 20;
    const skip = (page - 1) * 10;
    const queryobject = { authorId: id, state: state }
    const articles = await Article.find(queryobject).skip(skip).limit(limit);
    if (!articles) return errorResponse(res, 404, 'Article not found');
    return successResponse(res, 200, 'article fetched successfully', { articleNumber: articles.length, page: page, article: articles });
  } catch (error) {
    handleError(error, req);
    return errorResponse(res, 500, 'Server error');
  }
}

module.exports = {
  createArticle,
  getAllArticles,
  getAllArticleById,
  editArticle,
  updateArticle,
  deleteArticle,
  getAllUserArticle,

}

```

Let us now create our article routes.we create an article route file in the routes folder

## article route

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

const articleRouter = express.Router();

const {
  createArticle,
  getAllArticles,
  editArticle,
  getAllArticleById,
  updateArticle,
  deleteArticle,
  getAllUserArticle,
} = require('../controllers/articlecontroller');
const { authguard } = require('../middleware/auth');

articleRouter.route('/').post(authguard, createArticle).get(getAllArticles);
articleRouter.route('/userblog').get(authguard, getAllUserArticle);
articleRouter
  .route('/:articleId')
  .patch(authguard, editArticle)
  .get(getAllArticleById)
  .put(authguard, updateArticle)
  .delete(authguard, deleteArticle);

module.exports = articleRouter;
```

we now update our app.js file with our article router

## app.js

```
const dotenv = require('dotenv');
dotenv.config();

const express = require('express');

const userRouter = require('./routes/userroute');
const articleRouter = require('./routes/articleroute');


const app = express();

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

app.get('/',(req,res) => {
  res.send('welcome to my blog application');
})

app.use('/api/v1/users', userRouter);
app.use('/api/v1/articles',articleRouter);


module.exports = app;
```
once we are done with updating your app.js file,we can  start the server and start testing our different endpoints.

Thanks for reading and please leave a like or a share if it is helpful. Don't hesitate to ask any questions in the comments below.you can also view the project repository at https://github.com/omotega/alt-blog-api.