A step-by-step Guide to Creating a Blogging API/App with Nodejs/Express and MongoDb
Osamuyi
Posted on January 10, 2023
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
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
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}`)
})
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 };
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()
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;
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;
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();
});
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',
});
});
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,
});
};
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;
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;
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;
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;
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');
});
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
Posted on January 10, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.