Restful API for a Blog Service with NodeJS Express, MongoDB and Mongoose
Table of contents
Introduction
In this tutorial article, we will create a simple blog that blog visitors can read blog posts available. This allows us to explore the operations that are common to almost every blog application, retrieving article content from a database.
Here, this article shows how you can create a “Dynamic Blog” using Nodejs, Express and MongoDB and some other libraries, which you can then populate with routes, models, controllers, and database calls using mongo cloud. In this case, we’ll use the tool to create the framework for our sample blog application, to which we’ll add all the other code step by step needed by the blog. The creating site is extremely simple, requiring only that you invoke the generator on the command line interface with a new project name. We would be working on a simple CRUD application.
This API should be able to:
Signup a user
Login a user
A user must be able to create a post
A user can update his/her post
A user can get all published posts
A user can also 6 delete a post either in draft or published state
Blog Setup
Create an empty folder for your project
Create or initialize your NodeJS project
Open an empty folder in the VScode (or any code editor || IDE)
Enter
npm init
in the open terminalFollow through with the prompt messages in the terminal to set up a new nodejs project. (You can enter app.js/server.js in the prompt for the entry point).
In your terminal, enter the code below to install the package
npm i express nodemon mongoose dotenv jsonwebtoken body-parser
we’d be installing more packages as we progress with the blogging app, after we’ve Successfully installed all the necessary packages, we proceed to set up files and folders to get our project running.
- We are going to create a .env file for environmental variables.
PORT = 4000
MONGODB_URL=mongodb+srv://tobisam:test1234@cluster0.5i3n4iw.mongodb.net/?retryWrites=true&w=majority
JWT_SECRET=5ehnnd'[e]ebedm'ldfinfrvhbf79t8u]7%$5dh`
A server.js file (our application entry set up)
const express = require("express"); require("dotenv").config(); const bodyParser = require("body-parser");
Creating our database:
install mongoose, a javascript package for MongoDB querying,
npm i mongoose
const mongoose = require("mongoose"); const MONGODB_URL = process.env.MONGODB_URL; // connect to mongodb function connectToMongoDB() { mongoose.connect(MONGODB_URL); mongoose.connection.on("connected", () => { console.log("Connected to MongoDB successfully!"); }); mongoose.connection.on("error", err => { console.log("Error connecting to MongoDB", err); }); } module.exports = { connectToMongoDB };
Model
This folder houses our database schemas.
Since we are using MongoDB for this project it is necessary to note that MongoDB stores data in JSON (object) format, unlike the regular table in traditional databases.
So the schema describes how the fields are stored in the database.
We are going to be creating some files inside the model's folder
user.js
install bcrypt for password hashing
npm i bcrypt
install a validator to validate your field entries in the schema
npm i validator
const mongoose = require("mongoose"); const bcrypt = require("bcrypt"); const validator = require("validator"); const { Schema } = mongoose; userSchema = new Schema({ first_name: { type: String, required: true }, last_name: { type: String, required: true }, email: { type: String, required: true, unique: [true, "email already existed, please try another mail"] }, password: { type: String, required: true } }); // static signup method userSchema.statics.signup = async function( email, password, first_name, last_name ) { // validation if (!email || !password) { throw Error("All fields must be filled"); } if (!validator.isEmail(email)) { throw Error("Email is not valid"); } if (!validator.isStrongPassword(password)) { throw Error("Password is not strong enough!"); } const exists = await this.findOne({ email }); if (exists) { throw Error("Email already in use!"); } const salt = await bcrypt.genSalt(10); const hash = await bcrypt.hash(password, salt); const user = await this.create({ email: email, password: hash, first_name, last_name }); return user; }; // static login method userSchema.statics.login = async function(email, password) { // validation if (!email || !password) { throw Error("All fields must be filled"); } const user = await this.findOne({ email }); if (!user) { throw Error("Incorrect Email!"); } const match = await bcrypt.compare(password, user.password); if (!match) { throw Error("Incorrect password!"); } return user; }; const userModel = mongoose.model("User", userSchema); module.exports = userModel;
- install mongoosePaginate, a javascript package that handles pagination,
npm i mongoose-paginate-v2
- install mongoosePaginate, a javascript package that handles pagination,
```javascript
const mongoose = require("mongoose");
const mongoosePaginate = require("mongoose-paginate-v2");
const Schema = mongoose.Schema;
const blogSchema = new Schema(
{
title: {
type: String,
required: true,
unique: [true, "title must be unique"]
},
description: {
type: String
},
body: {
type: String,
required: true
},
author: {
type: String
},
read_count: {
type: Number,
default: 0
},
reading_time: {
type: String
},
tags: {
type: String,
enum: ["draft", "published"]
}
},
{
timestamps: true
}
);
blogSchema.plugin(mongoosePaginate);
const blogModel = mongoose.model("Blog", blogSchema);
module.exports = blogModel;
```
CONTROLLERS, MIDDLEWARE AND ROUTERS
CONTROLLER
Controllers are responsible for handling incoming requests and sending back responses or it could also mean a function written to manipulate data. We are going to be creating a controllers folder in our project directory
USER CONTROLLER
const User = require("../models/users"); const jwt = require("jsonwebtoken"); const createToken = (_id, email) => { return jwt.sign({ id: _id, email: email }, process.env.JWT_SECRET, { expiresIn: "1h" }); }; // login user const loginUser = async (req, res) => { const { email, password } = req.body; try { const user = await User.login(email, password); // createToken const token = createToken(user._id, email); res.status(200).json({ message: "Login successful", email, token }); } catch (error) { res.status(404).json({ error: error.message }); } }; // signup user const signupUser = async (req, res) => { const { email, password, first_name, last_name } = req.body; try { const user = await User.signup(email, password, first_name, last_name); // create token const token = createToken(user._id, email); res.status(200).json({ message: "Signup was successful", user: { email, first_name, last_name, token } }); } catch (error) { res.status(400).json({ error: error.message }); } }; module.exports = { loginUser, signupUser };
BLOG CONTROLLER
const mongoose = require("mongoose"); const blogModel = require("../models/blogs"); const readingTime = require("reading-time"); // GET all blogs const updateOptionForSorting = require("../helpers/updateOptions"); const getAllBlogs = async (req, res) => { try { let { page, sortBy, tags, author, title } = req.query; const options = { limit: 20, collation: { locale: "en" }, lean: true }; let query = {}; console.log(page); if (page === undefined) { page = 1; } page = Number(page); if (!(typeof page === "number")) { return res.status(400).json({ error: "invalid page number specified" }); } options.page = page; if (tags !== undefined) { if (tags !== "draft" && tags !== "published") { return res.status(400).json({ error: "invalid tags specified" }); } query.tags = tags; } if (author !== undefined) { query.author = author; } if (title !== undefined) { query.title = title; } if (sortBy !== undefined) { if ( (sortBy !== "timestamp") & (sortBy !== "read-count") & (sortBy !== "read-time") ) { return res.status(400).json({ error: "invalid sortBy parameter" }); } updateOptionForSorting(sortBy, options); } console.log(options); console.log(query); const paginatedBlogs = await blogModel.paginate(query, options); return res.status(200).json({ data: paginatedBlogs.docs }); } catch (error) { res.status(404).json({ error: error.message }); } }; // GET a single blog const getABlog = async (req, res) => { const { id } = req.params; if (!mongoose.Types.ObjectId.isValid(id)) { return res.status(404).json({ error: "No such blog" }); } try { const blog = await blogModel.findById(id); let initialCount = blog.read_count; const newCount = initialCount + 1; await blogModel.updateOne({ read_count: newCount }); if (!blog) { return res.status(404).json({ error: "No such blog" }); } res.status(200).json(blog); } catch (error) { res.status(404).json({ error: error.message }); } }; // CREATE a new blog const createBlog = async (req, res) => { const { title, description, body, author, tags } = req.body; const stats = readingTime(body); const { text } = stats; try { const blog = await blogModel.create({ title, description, body, author, tags, reading_time: text }); res.status(200).json(blog); } catch (err) { res.status(400).json({ error: err.message }); } }; // UPDATE a blog const updateBlog = async (req, res) => { const { id } = req.params; const { body } = req; if (!mongoose.Types.ObjectId.isValid(id)) { return res.status(404).json({ error: "No such blog" }); } try { const blog = await blogModel.findByIdAndUpdate({ _id: id }, { ...body }); if (!blog) { return res.status(404).json({ error: "No such blog" }); } res.status(200).json(blog); } catch (error) { res.status(404).json({ error: err.message }); } }; // DELETE a blog const deleteBlog = async (req, res) => { const { id } = req.params; if (!mongoose.Types.ObjectId.isValid(id)) { return res.status(404).json({ error: "No such blog" }); } try { const blog = await blogModel.findByIdAndDelete({ _id: id }); if (!blog) { return res.status(404).json({ error: "No such blog" }); } res.status(200).json(blog); } catch (error) { res.status(404).json({ error: err.message }); } }; module.exports = { getAllBlogs, getABlog, createBlog, updateBlog, deleteBlog };
ROUTES
USER ROUTES
const express = require("express"); const userRouter = express.Router(); const { loginUser, signupUser } = require("../controllers/users"); // login route userRouter.post("/login", loginUser); // signup route userRouter.post("/signup", signupUser); module.exports = userRouter;
BLOG ROUTES
const express = require("express"); const blogRouter = express.Router(); const authenticate = require("../middlewares/authenticate"); const { getAllBlogs, getABlog, createBlog, updateBlog, deleteBlog } = require("../controllers/blogs"); // GET all Blogs blogRouter.get("/", getAllBlogs); // GET a Blog blogRouter.get("/:id", getABlog); // CREATE a Blogs blogRouter.post("/", authenticate, createBlog); // UPDATE all Blogs blogRouter.patch("/:id", authenticate, updateBlog); // DELETE all Blogs blogRouter.delete("/:id", authenticate, deleteBlog); module.exports = blogRouter;
MIDDLEWARE
AUTHENTICATE
const jwt = require("jsonwebtoken"); const authenticate = async (req, res, next) => { let token = req.get("Authorization"); if (token === null || token === undefined) { return res.status(403).json({ error: "forbidden" }); } token = req.get("Authorization").replace("Bearer ", "").trim(); try { const userDetails = await jwt.verify(token, process.env.JWT_SECRET); } catch (error) { return res.status(401).json({ error: "not authorized" }); } next(); }; module.exports = authenticate;
I hope you found the article useful, you can check out the full project repository on GitHub: https://github.com/tobisamcode