DEV Community

Osamuyi
Osamuyi

Posted on

A step-by-step Guide to Creating a Blogging API/App with Nodejs/Express and MongoDb

A blog is a web page that is frequently updated, and which can be used for personal personal commentary or business content.

The features of the Api will include

  • Users (logged in or not) will be able able to access the homepage which displays a list of all published blog articles
  • Users will provide their email and password to login
  • Emails used for registering to the application will be unique
  • Users will have the option to register/sign-up so they can have the right to update and publish blog articles
  • Before anyone can login to the blog, an authentication will first be carried out to ensure that the user's credentials are available in the database, before access can be granted to such user.

To get started, in your terminal initialize an empty Node.js project with default settings:

npm init -y
Enter fullscreen mode Exit fullscreen mode

installation
Then, let's install the Express framework, and some other depencies for the project:

npm install express body-parser dotenv jsonwebtoken bcrypt mongoose validator  --save
Enter fullscreen mode Exit fullscreen mode

Body-parser middleware helps parse incoming requests from the body of the request before it gets to the handler.
dotenv package serves as a file that helps save sensitive information such as passwords, API keys; out of the main code
bcrypt: is a function hashing function, that helps in hashing a password before it is saved in the database. This will make it difficult for malicious hackers to be able to decrypt the passwords if they get access to the database.
jsonwebtoken: is used for both authentication and authorization. We would get into it eventually in this article.
mongoose; is an ORM used to query and manipulate data in the database. For the purpose of this project, we'll be working with MongoDb.

index.js
This is going to be the entry point for our blog api. So create a file called index.js in the root folder, and type in the following code

const express = require('express')
const bodyParser = require('body-parser')

const app = express()
const PORT = 3333;

app.get('/api', function (req, res) {
  return res
    .status(201)
    .json({ test_page: 'A step further to becoming a worldclass developer' });
});


app.listen(PORT, () => {
  console.log(`Server listening on port: ${PORT}`)
})
Enter fullscreen mode Exit fullscreen mode

In the code above, we required the express function, and then created an express server. After which, a custom route handler was created with the app.get() function. Now if you restart your server and go to http://localhost:3333/api, you will get a custom message displayed in the response header.

Database configuration
Create a *config * folder in the root directory of your project. Then create a dbconfig.js file. It should contain the following code

const moogoose = require('mongoose');
require('dotenv').config();

const MONGODB_URI = process.env.MONGODB_URI;

// connect to mongodb
function connectToMongoDB() {
    moogoose.connect(MONGODB_URI);

    moogoose.connection.on('connected', () => {
        console.log('Connected to MongoDB successfully');
    });

    moogoose.connection.on('error', (err) => {
        console.log('Error connecting to MongoDB', err);
    })
}

module.exports = { connectToMongoDB };
Enter fullscreen mode Exit fullscreen mode

Ensure to provide a url for the MONGODB_URI variable in the dotenv file, and also a JWT_SECRET of your choice
Also, require the connectToMongoDB function in the index.js folder like this

require('./config/dbconfig').connectToMongoDB()
Enter fullscreen mode Exit fullscreen mode

Next, we create a model folder in the root directory of our project.
models
In here, we will be having two services/models. One for the user, and the other for the blog.
user_Model.js

const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const validator = require('validator');
const saltRounds = 10;

const Schema = mongoose.Schema;

const userSchema = new Schema(
  {
    firstname: {
      type: String,
      required: [true, 'Your First name is required'],
    },
    lastname: {
      type: String,
      required: [true, 'Your last name is required'],
    },
    email: {
      type: String,
      required: [true, 'Your email address is required'],
      unique: [true, 'This email already exists'],
      lowercase: true,
      validate: [validator.isEmail, 'Provide a valid email address'],
    },
    password: {
      type: String,
      required: true,
      minlength: [8, 'Password must be at least 8 characters long'],
      select: false,
    },
    articles: [
      {
        type: Schema.Types.ObjectId,
        ref: 'Blog',
      },
    ],
  },
  { timestamps: true }
);

// Using bcrypt to hash user password before saving into database
userSchema.pre('save', function (next) {
  const user = this;

  bcrypt.hash(user.password, saltRounds, (err, hash) => {
    user.password = hash;
    next();
  });
});

const User = mongoose.model('user', userSchema);
module.exports = User;

Enter fullscreen mode Exit fullscreen mode

In the above code, firstly we created a mongoose user schema, to help us define the structure of our document. Note that we referenced the blog in the article property. This is to makesure there is a link between a blog article and the user that posted the blog.
Also note the pre('save') function at the bottom of the schema, it ensures that the password given by the user when registering is hashed before is can be saved into the database. We achieved this using the external library which was installed earlier in the project "bcrypt".
blog_Model.js

const mongoose = require('mongoose');
const user = require('./users_model');

const Schema = mongoose.Schema;

//BlogPost schema
const BlogPostSchema = new Schema(
  {
    title: {
      type: String,
      required: [true, 'Title missing, provide a title'],
      unique: [true, 'Title name already exists'],
    },
    description: {
      type: String,
      required: [true, 'Description missing, provide a description'],
    },
    author: {
      type: Schema.Types.ObjectId,
      ref: 'User',
      required: true,
    },
    state: {
      type: String,
      enum: ['Draft', 'Published'],
      default: 'Draft',
    },
    read_count: {
      type: Number,
      default: 0,
    },
    reading_time: {
      type: String,
      required: [true, 'Provide a reading_time'],
    },
    tags: {
      type: String,
      required: [true, 'Specify tags'],
    },
    body: {
      type: String,
      required: [true, 'Body is needed'],
    }
  },
  {
    timestamps: true,
  }
);

const Blog = mongoose.model('BlogPost', BlogPostSchema);
module.exports = Blog;

Enter fullscreen mode Exit fullscreen mode

Controller
The controller will contain the logic for our project. We will created three files in our controller folder: userController, BlogpostController and ErrorController

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

const util = require('util');

const userModel = require('../models/users_model');
const tryCatchErr = require('../utilities/catchErrors');
const serverErr = require('../utilities/serverError');
const CONFIG = require('../config/config');

const JWT_SECRET = CONFIG.JWT_SECRET;

//Create token function and set expiration to 1hr
const signInToken = (id) => {
  return jwt.sign({ id }, JWT_SECRET, { expiresIn: '1h' });
};

////////////////////////////////////////////////////////////////
/*
 * SIGN IN NEW USER
 */
/////////////////////////////////////////////////////////////////

exports.createUser = tryCatchErr(async (req, res, next) => {
  const user = await userModel.create({
    firstname: req.body.firstname,
    lastname: req.body.lastname,
    email: req.body.email,
    password: req.body.password,
  });

  const token = signInToken(user._id);
  res.status(200).json({
    status: 'success',
    token,
    data: {
      user: user,
    },
  });
});

////////////////////////////////////////////////////////////////
/*
 * LOGIN IN USER
 */
/////////////////////////////////////////////////////////////////

exports.login = tryCatchErr(async (req, res, next) => {
  const { email, password } = req.body;


  const user = await userModel.findOne({ email })
  if (!user) {
    return next(new serverErr('User not found', 401));
  }
  bcrypt.compare(password, user.password, (error, result) => {
  if (error) return next (error)
  if (!result) {
     return next(new serverErr('Wrong email and/or password', 401));
}
};

  const token = signInToken(user._id);
  res.status(201).json({
    status: 'success',
    token,
  });
});

////////////////////////////////////////////////////////////////////////
/*
 * Authenticating routes
 */
///////////////////////////////////////////////////////////////////////

exports.authenticate = tryCatchErr(async (req, res, next) => {
  let bearerToken;
  if (
    req.headers.authorization &&
    req.headers.authorization.startsWith('Bearer')
  ) {
    bearerToken = req.headers.authorization.split(' ')[1];
  }
  if (!bearerToken) {
    return next(new serverErr('Unauthirized, Login to continue', 401));
  }

  //Verify token
  const user = await util.promisify(jwt.verify)(bearerToken, JWT_SECRET);

  //Confirm if user still exists
  const currentUser = await userModel.findById(user.id);
  if (!currentUser) {
    return next(new serverErr('User does not exist', 401));
  }
  req.user = currentUser;
  next();
});

Enter fullscreen mode Exit fullscreen mode

blog_controller.js

const blogModel = require('../models/blog_model');
const userModel = require('../models/users_model');
//const userService = require('../Services/UserServices');

const tryCatchErr = require('../utilities/catchErrors');
const serverError = require('../utilities/serverError');

require('dotenv').config();

///////////////////////////////////////////////////////////////
/*
 * Get all blog posts
 *
 */

///////////////////////////////////////////////////////////////

exports.getAllBlogs = tryCatchErr(async (req, res, next) => {
  const objectQuerry = { ...req.query };

  /// Filteration
  const removedFields = ['page', 'sort', 'limit', 'fields'];
  removedFields.forEach((field) => delete objectQuerry[field]);

  let query = blogModel.find(objectQuerry);

  // Sorting
  if (req.query.sort) {
    const sortParams = req.query.sort.split(',').join(' ');
    query = query.sort(sortParams);
  } else {
    // sorting by the most recent blog posted
    query = query.sort('-createdAt');
  }

  //Pagination
  const page = req.query.page * 1 || 1;
  const limit = req.query.limit * 1 || 20;
  const skip = (page - 1) * limit;

  if (req.query.page) {
    const numArticles = await blogModel
      .countDocuments()
      .where({ state: 'Published' });
    if (skip >= numArticles) {
      throw new serverError('Page does not exist', 404);
    }
  }

  query = query.skip(skip).limit(limit);

  //Displaying a single published blop post
  const publishedBlogPost = await blogModel
    .find(query)
    .where({ state: 'Published' })
    .populate('user', { firstname: 1, lastname: 1, _id: 1 });

  res.status(200).json({
    status: 'success',
    result: publishedBlogPost.length,
    curentPage: page,
    limit: limit,
    totalPages: Math.ceil(publishedBlogPost.length / limit),
    data: {
      publishedBlogPost,
    },
  });
});

////////////////////////////////////////////////////////////////
/*
 * CREAT A NEW BLOG ARTICLE
 * route: get /api/blogs
 */
////////////////////////////////////////////////////////////////
exports.creatBlog = tryCatchErr(async (req, res, next) => {
  const { title, description, state, tags, body } = req.body;

  if (!title || !description || !state || !tags || !body) {
    return next(new serverError('Provide all required information', 401));
  }

  // Get/Authenticate the user creating the blog
  const user = await userModel.findById(req.user._id);
  console.log(req.user._id);

  //Calculating the average read time of the blog
  let avgWPM = 250;
  const readTime = Math.ceil(body.split(/\s+/).length / avgWPM);
  const reading_time =
    readTime < 1 ? `${readTime + 1} minute read` : `${readTime} minutes read`;

  const author = `${user.firstname} ${user.lastname}`;

  const newblogArticle = new blogModel({
    title: title,
    description: description,
    author: req.user._id,
    reading_time: reading_time,
    state: state,
    tags: tags,
    body: body,
    user: user._id,
  });

  //   //save the blog article
  const savedBlogArticle = await newblogArticle.save();

  //Add the article to the author's blogs
  user.articles = user.articles.concat(savedBlogArticle._id);

  await user.save();
  res.status(201).json({
    message: 'Blog Article Created successfully',
    data: {
      blog: savedBlogArticle,
    },
  });
});

/////////////////////////////////////// ////////////////////////
/*
 * Get a single blog post
 * Get /api/blogs/:id
 */
////////////////////////////////////////////////////////////////

exports.getBlogById = tryCatchErr(async (req, res, next) => {
  const blog = await blogModel
    .findById(req.params.id)
    .where({ state: 'Published' })
    .populate('user', { firstname: 1, lastname: 1, _id: 1 });

  if (!blog) {
    return next(new serverError('Blog article not found', 404));
  }

  //Updating the read count
  blog.read_count += 1;

  //Save to DB
  blog.save();

  res.status(201).json({
    status: 'Success',
    blog,
  });
});

//////////////////////////////////////////////////////////////////
/*
 * Get all blog posts by user ID
 * Get /api/blogs
 */
//////////////////////////////////////////////////////////////////
exports.getUserArticle = tryCatchErr(async (req, res, next) => {
  const user = req.user;

  const queryObject = { ...req.query };

  const removedFields = ['page', 'sort', 'limit'];
  removedFields.forEach((field) => delete queryObject[field]);

  let blogQuerry = blogModel.find({ user });

  // Sorting
  if (req.blogQuerry.sort) {
    const sortBy = req.blogQuerry.sort.split(',').join(' ');
    blogQuerry = blogQuerry.sort(sortBy);
  } else {
    blogQuerry = blogQuerry.sort('-createdAt'); // default sorting : starting from the most recent
  }

  // Pagination
  // convert to number and set default value to 1
  const page = req.blogQuerry.page * 1 || 1;
  const limit = req.blogQuerry.limit * 1 || 20;
  const skip = (page - 1) * limit;

  if (req.blogQuerry.page) {
    const numArticles = await blogQuerry.countDocuments();
    if (skip >= numArticles)
      throw new serverError('This page does not exist', 404);
  }
  blogQuerry = blogQuerry.skip(skip).limit(limit);

  blogQuerry = blogQuerry.populate('user', {
    firstname: 1,
    lastname: 1,
    _id: 1,
  });

  const articles = await blogQuerry;

  return res.status(200).json({
    status: 'success',
    result: articles.length,
    data: {
      articles: articles,
    },
  });
});

/////////////////////////////////////////////////////////////////////
/*
 * Update blog post by Author

 */
////////////////////////////////////////////////////////////////////////////////////////////////

exports.updateBlog = tryCatchErr(async (req, res, next) => {
  const { title, description, state, tags, body } = req.body;

  const user = req.user;

  const blogPostId = await blogModel.findById(req.params.id);
  //confirm user credentials
  if (user.id !== blogPostId.user._id.toString()) {
    return next(
      new serverError('Authorization is required to update this document', 401)
    );
  }

  //Update Blog
  const updatedBlogPost = await blogModel.findByIdAndUpdate(
    { _id: req.params.id },
    {
      $set: {
        title: title,
        description: description,
        state: state,
        tags: tags,
        body: body,
      },
    },
    {
      new: true,
    }
  );
  res.status(201).json({
    status: 'Success',
    data: {
      updatedBlogPost,
    },
  });
});

//////////////////////////////////////////////////////////////////////
/*
 *Delete blog post by Author
 * Protected route
 */
//////////////////////////////////////////////////////////////////////
exports.deleteBlog = tryCatchErr(async (req, res, next) => {
  const user = req.user;

  const blogPostId = await blogModel.findById(req.params.id);
  const blogAuthor = await userModel.findById(user.id);

  //console.log(blogPostId, blogAuthor);

  if (user.id !== blogPostId.user._id.toString()) {
    return next(
      new serverError('Authorization is required to delete this document')
    );
  }
  await blogModel.findByIdAndDelete(req.params.id);

  const index = blogAuthor.articles.indexOf(req.params.id);
  if (index === -1) {
    return next(new serverError('Blog post not found', 404));
  }
  blogAuthor.articles.splice(index, 1);
  await blogAuthor.save();

  res.status(201).json({
    status: 'Success',
    message: 'Blog post deleted successfully',
  });
});

Enter fullscreen mode Exit fullscreen mode

error_controller.js

module.exports = (err, req, res, next) => {
  err.statusCode = err.statusCode || 500;
  err.status = err.status || 'error';
  res.status(err.statusCode).json({
    status: err.status,
    message: err.message,
  });
};

Enter fullscreen mode Exit fullscreen mode

Routes
In the route folder, create a "user_route.js" and "blog_route.js" files
user_route.js

 const express = require('express');

const { createUser, login } = require('./../controllers/users_controller');

const router = express.Router();

router.post('/register', createUser);
router.post('/authenticate', login);

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

blog_route.js

const express = require('express');

const {
  getAllBlogs,
  creatBlog,
  getBlogById,
  getUserArticle,
  updateBlog,
  deleteBlog,
} = require('../controllers/blog_controller');
const { authenticate } = require('../controllers/users_controller');

const router = express.Router();

router.route('/').get(getAllBlogs).post(authenticate, creatBlog);

router.route('/:id').get(getBlogById);

router
  .route('/blog_aticles/:id')
  .get(authenticate, getUserArticle)
  .put(authenticate, updateBlog)
  .delete(authenticate, deleteBlog);

module.exports = router;

Enter fullscreen mode Exit fullscreen mode

In the project's root folder, create a utilites folder will store our errors. It will contain two files "catchErrors.js" and "serverError.js"
catchError.js

const tryCatchError = (name) => {
  return (req, res, next) => {
    name(req, res, next).catch(next);
  };
};

module.exports = tryCatchError;

Enter fullscreen mode Exit fullscreen mode

serverError.js

class serverErr extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
    this.isOperational = true;
    Error.captureStackTrace(this, this.constructor);
  }
}

module.exports = serverErr;

Enter fullscreen mode Exit fullscreen mode

Now, we update our index.js file.

const express = require('express');
const logger = require('morgan');
const mongoose = require('mongoose');
const bodyParser = require('body-parser');

const users = require('./app/api/routes/user_routes');
const blogs = require('./app/api/routes/blog_route');
const CONFIG = require('./app/api/config/config');
const connectToDb = require('./app/api/Db/mongodb');
const errorHandler = require('./app/api/controllers/error_controllers');
const serverError = require('././app/api/utilities/serverError');

const app = express();
app.use(logger('dev'));
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

//CONNECT to MongoDB
connectToDb();

app.use('/api/users', users);
app.use('/api/blogs', blogs);

app.get('/api', function (req, res) {
  return res
    .status(201)
    .json({ test_page: 'A step further to becoming a worldclass developer' });
});

//Undefined route error handler
app.all('*', function (req, res, next) {
  next(new serverError('Undefined route, page not found.', 404));
});

//HANDLE ERROR
app.use(errorHandler);

app.listen(CONFIG.PORT, () => {
  console.log('Node server listening on port 3000');
});

Enter fullscreen mode Exit fullscreen mode

Now, we can connect to our database and test our code.

Conclusion
In this project we created a blog API that performs basic CRUD operations, using Express.js, MongoDb and jsonwebtoken for authentication. I would appreciate your reviews. Thank you

Top comments (0)