On this page
Tracks

NodeJS Blogging Application

Last reviewed 2 Sept 2026

Setup the project (Create project using npm init, install package that you remember that we are going to use and also nodemon in dev and update package.json for npm start nodemon index.js)

Install the package

npm i express mongoose ejs jsonwebtoken dotenv cookie-parser multer

npm i nodemon -D

Create all the folder controller, routes, views, service,middleware, models

index.js

const express = require("express");
const path = require("path")
const app = express();
const PORT= 8000;
app.set("view engine","ejs");
app.set("views",path.resolve("./views"));
app.use(express.json());
app.use(express.urlencoded({extended:false}))
app.get("/", (req, res)=>{
return res.render("home")

})

app.listen(PORT, ()=>{
console.log("Server started at ", PORT);

})

Create views/home.ejs

<html lang="en">

<head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

Blog App

</head>

<body>

Homepage

</body>

</html>

Now we will use Bootstrap for html and css pages. So we are going to use CDN but we can’t use in every page so we will use ejs feature which is Partial

Partials come in handy when you want to reuse the same HTML across multiple views.

Let’s create two files in partials

Create views/partials/head.ejs

href=“https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css”

rel=“stylesheet”

integrity=“sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD”

crossorigin=“anonymous”

/>

<meta charset="UTF-8" />

<meta http-equiv="X-UA-Compatible" content="IE=edge" />

<meta name="viewport" content="width=device-width, initial-scale=1.0" />

Create views/partials/script.ejs

<script

src=“https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js”

integrity=“sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN”

crossorigin=“anonymous”

></script>

Update views/home.ejs

<html lang="en">

<%- include(’./partials/head’) %>

Homepage

</head>

<body>

Homepage

<%- include(’./partials/scripts’) %>

</body>

</html>

Since we need navbar on all pages so we will create partials

Create views/partials/nav.ejs

We just copied the code from bootstrap navbar. Also we have added bg-dark” data-bs-theme=“dark in nav tag https://getbootstrap.com/docs/4.1/components/navbar/. Also update the details as per our application.

Update views/home.ejs

Included the nav.ejs

<html lang="en">

<head>

<%- include(’./partials/head’) %>

Homepage

</head>

<body>

<%- include(’./partials/nav.ejs’) %>

Homepage

<%- include(’./partials/scripts’) %>

</body>

</html>

Create Signin and signup flow

We will need to use mongoose. Also we will need a default image if the user does not provide the image.

Create models/user.js

const { createHmac, randomBytes } = require("crypto");
const { Schema, model } = require("mongoose");
const userSchema = new Schema(

{

fullName: {
type: String,
required: true,

},

email: {
type: String,
required: true,
unique: true,

},

salt: {
type: String,

},

password: {
type: String,
required: true,

},

profileImageURL: {
type: String,
default: "/images/default.png",

},

role: {
type: String,
enum: ["USER", "ADMIN"],
default: "USER",

},

},

{ timestamps: true }

);

userSchema.pre(“save”, function (next) {

const user = this;
if (!user.isModified("password")) return;
const salt = randomBytes(16).toString();
const hashedPassword = createHmac("sha256", salt)

.update(user.password)

.digest("hex");
this.salt = salt;
this.password = hashedPassword;
next();
});
const User = model("user", userSchema);
module.exports = User;

Explanation:

Schema is understandable so we used

We used profile image url so we need to create a folder public/images and paste the download user avatar image here.

profileImageURL: {
type: String,
default: "/images/default.png",
}

We will have two roles for the user, User and Admin. Also we should have only 2 options so we have used enum here

role: {
type: String,
enum: ["USER", "ADMIN"],
default: "USER",

},

We are storing the salt(random string for hashing the password) of the user in the database

salt: {
type: String,

},

Below method will be called before we save details in the database.

userSchema.pre(“save”, function (next) {

const user = this;

if (!user.isModified(“password”)) return; // if user’s password is created/modified

const salt = randomBytes(16).toString(); // Generate 16 letter random string
const hashedPassword = createHmac("sha256", salt)

.update(user.password)

.digest("hex");

// Create hash password using salt in hexadecimal form

this.salt = salt;
this.password = hashedPassword;

//Above code to update the user details

next();
});

Create routes/user.js

**const { Router } = require("express");**
**const User = require("../models/user");**

const router = Router();

**router.get("/signin", (req, res) => {**

return res.render(“signin”);

});

**router.get("/signup", (req, res) => {**

return res.render(“signup”);

});

**router.post("/signup", async (req, res) => {**

const { fullName, email, password } = req.body;

await User.create({

fullName,

email,

password,

});

return res.redirect(”/”);

});

**module.exports = router;**

Create views/signup.ejs

<html lang="en">

<head>

<%- include(’./partials/head’) %>

Signup

</head>

<body>

<%- include(’./partials/nav’) %>

<input

type=“text”

class=“form-control”

id=“fullName”

name=“fullName”

aria-describedby=“fullName”

/>

<label for=“exampleInputEmail1” class=“form-label”

>Email address</label

>

<input

type=“email”

name=“email”

class=“form-control”

id=“exampleInputEmail1”

aria-describedby=“emailHelp”

/>

We’ll never share your email with anyone else.

<input

name=“password”

type=“password”

class=“form-control”

id=“exampleInputPassword1”

/>

<%- include(’./partials/scripts’) %>

</body>

</html>

Update index.js

**const express = require("express");**
**const path = require("path");**
**const userRoute = require("./routes/user");**
**const mongoose = require("mongoose");**

const app = express();

const PORT= 8000;

mongoose

.connect(‘mongodb://127.0.0.1:27017/blogit’)

**.then((e) => console.log("MongoDB Connected"));**

app.set(“view engine”,“ejs”);

app.set(“views”,path.resolve(”./views”));

app.use(express.json());

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

app.use(“/user”,userRoute);

**app.get("/", (req, res)=>{**

return res.render(“home”)

})

**app.listen(PORT, ()=>{**
**console.log("Server started at ", PORT);**

})

Create service/authentication.js

Add authentication using jsonwebtoken

**const JWT = require("jsonwebtoken");**

const secret = “$uperMan@123”;

function createTokenForUser(user) {

const payload = {

_id: user._id,

email: user.email,

profileImageURL: user.profileImageURL,

role: user.role,

};

const token = JWT.sign(payload, secret);

return token;

}

function validateToken(token) {

const payload = JWT.verify(token, secret);

return payload;

}

**module.exports = {**

createTokenForUser,

validateToken,

};

Update models/user.js

**const { createHmac, randomBytes } = require("crypto");**
**const { Schema, model } = require("mongoose");**
**const { createTokenForUser } = require("../service/authentication");**

const userSchema = new Schema(

{

fullName: {

type: String,

required: true,

},

email: {

type: String,

required: true,

unique: true,

},

salt: {

type: String,

},

password: {

type: String,

required: true,

},

profileImageURL: {

type: String,

default: “/images/default.png”,

},

role: {

type: String,

enum: [“USER”, “ADMIN”],

default: “USER”,

},

},

{ timestamps: true }

);

userSchema.pre(“save”, function (next) {

const user = this;

if (!user.isModified(“password”)) return;

const salt = randomBytes(16).toString();

const hashedPassword = createHmac(“sha256”, salt)

.update(user.password)

.digest(“hex”);

this.salt = salt;

this.password = hashedPassword;

next();

});

userSchema.static(

“matchPasswordAndGenerateToken”,

async function (email, password) {

const user = await this.findOne({ email });

if (!user) throw new Error(“User not found!”);

const salt = user.salt;

const hashedPassword = user.password;

const userProvidedHash = createHmac(“sha256”, salt)

.update(password)

.digest(“hex”);

if (hashedPassword !== userProvidedHash)

throw new Error(“Incorrect Password”);

const token = createTokenForUser(user);

return token;

}

);

const User = model(“user”, userSchema);

**module.exports = User;**

Added the virtual function (static function) to the user schema to verify the password

Update routes/user.js

const { Router } = require("express");
const User = require("../models/user");
const router = Router();
router.get("/signin", (req, res) => {
return res.render("signin");
});
router.get("/signup", (req, res) => {
return res.render("signup");
});
router.post("/signin", async (req, res) => {
const { email, password } = req.body;
try {
const token = await User.matchPasswordAndGenerateToken(email, password);
return res.cookie("token", token).redirect("/");

} catch (error) {

return res.render("signin", {
error: "Incorrect Email or Password",
});
}
});
router.get("/logout", (req, res) => {
res.clearCookie("token").redirect("/");
});
router.post("/signup", async (req, res) => {
const { fullName, email, password } = req.body;
await User.create({

fullName,

email,

password,

});
return res.redirect("/");
});
module.exports = router;

Added sign method which will use userschema static method (models/user.js) matchPasswordAndGenerateToken to verify the password and return the jwtToken generated using service/authentication.js

Now we need to create Sign in page as well

Create views/signin.ejs

<html lang="en">

<head>

<%- include(’./partials/head’) %>

Signin

</head>

<body>

<%- include(’./partials/nav’) %>

<label for=“exampleInputEmail1” class=“form-label”

>Email address</label

>

<input

type=“email”

name=“email”

class="form-control"

id=“exampleInputEmail1”

aria-describedby=“emailHelp”

/>

We’ll never share your email with anyone else.

<input

name=“password”

type=“password”

class="form-control"

id=“exampleInputPassword1”

/>

<%- include(’./partials/scripts’) %>

</body>

</html>

Update views/partials/nav.ejs

Here we are showing the error message

<% if (locals.error) { %>

<% } %>

Create middleware/authentication.js

const { validateToken } = require("../services/authentication");
function checkForAuthenticationCookie(cookieName) {
return (req, res, next) => {
const tokenCookieValue = req.cookies[cookieName];

if (!tokenCookieValue) {

return next();
}
try {
const userPayload = validateToken(tokenCookieValue);
req.user = userPayload;

} catch (error) {

console.log(error);
}
return next();
};
}
module.exports = {

checkForAuthenticationCookie,

};

Update index.js

Added middleware for authentication and cookie parse. Also passed the object in the response to homepage so that we can add logic to hide nav menu like Add blog, name, etc

const express = require("express");
const path = require("path");
const userRoute = require("./routes/user");
const mongoose = require("mongoose");
const cookieParser = require("cookie-parser");
const { checkForAuthenticationCookie } = require("./middleware/authentication");
const app = express();
const PORT= 8000;

mongoose

.connect(‘mongodb://127.0.0.1:27017/blogit’)

.then((e) => console.log("MongoDB Connected"));
app.set("view engine","ejs");
app.set("views",path.resolve("./views"));
app.use(express.urlencoded({extended:false}))
app.use(cookieParser());
app.use(checkForAuthenticationCookie("token"));
app.get("/", (req, res)=>{
return res.render("home",{
user: req.user

})

})

app.use("/user",userRoute);
app.listen(PORT, ()=>{
console.log("Server started at ", PORT);

})

Update views/partials/nav.ejs

Hide

class="navbar navbar-expand-lg bg-body-tertiary bg-dark"

data-bs-theme=“dark”

>

Blogit

<button

class="navbar-toggler"

type=“button”

data-bs-toggle=“collapse”

data-bs-target=“#navbarNavDropdown”

aria-controls=“navbarNavDropdown”

aria-expanded=“false”

aria-label=“Toggle navigation”

>

<% if (locals.error) { %>

<% } %>

Add blog and comments flow

Create models/blog.js

const { Schema, model } = require("mongoose");
const blogSchema = new Schema(

{

title: {
type: String,
required: true,

},

body: {
type: String,
required: true,

},

coverImageURL: {
type: String,
required: false,

},

createdBy: {
type: Schema.Types.ObjectId,
ref: "user",

},

},

{ timestamps: true }

);
const Blog = model("blog", blogSchema);
module.exports = Blog;

We use below code to refer to the user table.

createdBy: {
type: Schema.Types.ObjectId,
ref: "user",

},

Create views/addBlog.js

<html lang="en">

<head>

<%- include(’./partials/head’) %>

Add New Blog

</head>

<body>

<%- include(’./partials/nav’) %>

<input

type=“file”

class=“form-control”

id=“coverImage”

name=“coverImage”

aria-describedby=“coverImage”

/>

<input

type=“text”

class=“form-control”

id=“title”

name=“title”

aria-describedby=“title”

/>

<%- include(’./partials/scripts’) %>

</body>

</html>

Create routes/blog.js

**const { Router } = require("express");**
**const multer = require("multer");**
**const path = require("path");**
**const Blog = require("../models/blog");**

const router = Router();

const storage = multer.diskStorage({

destination: function (req, file, cb) {

cb(null, path.resolve(./public/uploads/));

},

filename: function (req, file, cb) {

const fileName = ${Date.now()}-${file.originalname};

cb(null, fileName);

},

});

const upload = multer({ storage: storage });

**router.get("/add-new", (req, res) => {**

return res.render(“addBlog”, {

user: req.user,

});

});

**router.post("/", upload.single("coverImage"), async (req, res) => {**

const { title, body } = req.body;

const blog = await Blog.create({

body,

title,

createdBy: req.user._id,

coverImageURL: `/uploads/${req.file.filename}`,

});

return res.redirect(/blog/${blog._id});

});

**module.exports = router;**

Update index.js

Add routes for blog and render the blogs on homepage so we need to pass it to the home.ejs

const express = require("express");
const path = require("path");
const userRoute = require("./routes/user");
const blogRoute = require("./routes/blog");
const mongoose = require("mongoose");
const cookieParser = require("cookie-parser");
const { checkForAuthenticationCookie } = require("./middleware/authentication");
const Blog = require("./models/blog");
const app = express();
const PORT = 8000;

mongoose

.connect(“mongodb://127.0.0.1:27017/blogit”)

.then((e) => console.log("MongoDB Connected"));
app.set("view engine", "ejs");
app.set("views", path.resolve("./views"));
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(checkForAuthenticationCookie("token"));
app.use(express.static(path.resolve("./public")));
app.get("/", async (req, res) => {
const allBlogs = await Blog.find({});
res.render("home", {
user: req.user,
blogs: allBlogs,
});
});
app.use("/user", userRoute);
app.use("/blog", blogRoute);
app.listen(PORT, () => {
console.log("Server started at ", PORT);
});

Now render the blogs on homepage

Update views/home.ejs

<html lang="en">

<head>

<%- include(’./partials/head’) %>

Homepage

</head>

<body>

<%- include(’./partials/nav’) %>

**<% blogs.forEach(blog => { %>**

<%= blog.title %>

View

<% }) %>

<%- include(’./partials/scripts’) %>

</body>

</html>

Create views/blog.ejs

<html lang="en">

<head>

<%- include(’./partials/head’) %>

Blog

</head>

<body>

<%- include(’./partials/nav’) %>

<%= blog.title %>

<%= blog.body %>

<%=

blog.createdBy.fullName %>

Comments (<%= comments.length %>)

<% if (locals.user) { %>

<input

type=“text”

name=“content”

class=“form-control”

placeholder=“Enter your comment”

/>

<% } %>

**<% comments.forEach(comment => { %>**

<%= comment.createdBy.fullName %>

<%= comment.content %>

<% }) %>

<%- include(’./partials/scripts’) %>

</body>

</html>

Create models/comment.js

**const { Schema, model } = require("mongoose");**

const commentSchema = new Schema(

{

content: {

type: String,

required: true,

},

blogId: {

type: Schema.Types.ObjectId,

ref: “blog”,

},

createdBy: {

type: Schema.Types.ObjectId,

ref: “user”,

},

},

{ timestamps: true }

);

const Comment = model(“comment”, commentSchema);

**module.exports = Comment;**

Update routes/blog.js

**const { Router } = require("express");**
**const multer = require("multer");**
**const path = require("path");**
**const Blog = require("../models/blog");**
**const Comment = require("../models/comment");**

const router = Router();

const storage = multer.diskStorage({

destination: function (req, file, cb) {

cb(null, path.resolve(./public/uploads/));

},

filename: function (req, file, cb) {

const fileName = ${Date.now()}-${file.originalname};

cb(null, fileName);

},

});

const upload = multer({ storage: storage });

**router.get("/add-new", (req, res) => {**

return res.render(“addBlog”, {

user: req.user,

});

});

**router.get("/:id", async (req, res) => {**

const blog = await Blog.findById(req.params.id).populate(“createdBy”);

const comments = await Comment.find({ blogId: req.params.id }).populate(

“createdBy”

);

return res.render(“blog”, {

user: req.user,

blog,

comments,

});

});

**router.post("/comment/:blogId", async (req, res) => {**

await Comment.create({

content: req.body.content,

blogId: req.params.blogId,

createdBy: req.user._id,

});

return res.redirect(/blog/${req.params.blogId});

});

**router.post("/", upload.single("coverImage"), async (req, res) => {**

const { title, body } = req.body;

const blog = await Blog.create({

body,

title,

createdBy: req.user._id,

coverImageURL: `/uploads/${req.file.filename}`,

});

return res.redirect(/blog/${blog._id});

});

**module.exports = router;**

Populate method: It is used to get the details of the model. Here we are populate createdBy which is user table so in blog, we will get user record details as well

const blog = await Blog.findById(req.params.id).populate(“createdBy”);

const comments = await Comment.find({ blogId: req.params.id }).populate(

“createdBy”

);