DEV Community

Cover image for Building a blog Api with Nodejs
omotega
omotega

Posted on • Updated on

Building a blog Api with Nodejs

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
Enter fullscreen mode Exit fullscreen mode

Navigate to the folder

cd blog-api
Enter fullscreen mode Exit fullscreen mode

Initialize the project

yarn init -y
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

yarn add nodemon -D
Enter fullscreen mode Exit fullscreen mode

When the packages are installed, make a file called .env.

touch .env
Enter fullscreen mode Exit fullscreen mode

Go to mongodb website 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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

mkdir config
touch db.js
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
};

Enter fullscreen mode Exit fullscreen mode

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,
};

Enter fullscreen mode Exit fullscreen mode

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,
};

Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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,
};

Enter fullscreen mode Exit fullscreen mode

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,

}

Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)