Here we will see how to build a simple CRUD application in Node JS with MongoDB.
1. Setup the project folder
First, create a new folder on your desktop or wherever you want and name it node-mongo-crud-app
, this will be our project folder.
After that, run npm init -y
on your terminal to initialize the npm into this folder, and then –
Install the following Node packages –
express
express-validator
– for form validation.esj
– template engine.mongoose
– Object Modeling tool for MongoDB.
npm i express express-validator ejs mongoose
In this project, we will use es6 import, so you have to add "type": "module"
in the package.json
.

2. Building the Node MongoDB CRUD app
Application folder structure
Here given in the below image are the files and folders that we have to create to build this Node JS MongoDB CRUD Application.

Post Modal
This app will perform CRUD operations with posts like – Create Post
, Read Post
, Update Post
and Delete Post
.
Here we use Mongoose to interact with MongoDB. Mongoose is an Object Data Modeling (ODM) library, therefore we have to create a Model for the posts collection where we have to define the post schema.
import mongoose from "mongoose";
const postSchema = new mongoose.Schema(
{
title: {
type: String,
require: true,
},
content: {
type: String,
require: true,
},
author: {
type: String,
require: true,
},
},
{
timestamps: true,
}
);
const Post = mongoose.model("Post", postSchema);
export default Post;
Routes and Controllers
routes.js
β contains all the routes with validation rules.Controller.js
β The Controller class contains all the callbacks that handle all the requests and perform all CRUD operations.
- routes.js
- Controller.js
import { Router } from "express";
import { body } from "express-validator";
import Ctrl from "./Controller.js";
const router = Router({ strict: true });
const validation_rule = [
body("title", "Must not be empty.")
.trim()
.not()
.isEmpty()
.unescape()
.escape(),
body("author", "Must not be empty.")
.trim()
.not()
.isEmpty()
.unescape()
.escape(),
body("content", "Must not be empty")
.trim()
.not()
.isEmpty()
.unescape()
.escape(),
];
// Show all posts
router.get("/", Ctrl.all_posts);
// Create a New Post
router
.get("/create", Ctrl.create_post_page)
.post("/create", validation_rule, Ctrl.validation, Ctrl.insert_post);
// Show a single post by the ID
router.get("/post/:id", Ctrl.id_validation, Ctrl.single_post_page);
// Edit a Post
router
.get("/edit/:id", Ctrl.id_validation, Ctrl.edit_post_page)
.post("/edit/:id", validation_rule, Ctrl.id_validation, Ctrl.update_post);
// Delete a Post
router.get("/delete/:id", Ctrl.id_validation, Ctrl.delete_post);
export default router;
import { validationResult, matchedData } from "express-validator";
import Post from "./PostModel.js";
import { Types } from "mongoose";
const ObjectID = Types.ObjectId; // to check valid ObjectID
const validation_result = validationResult.withDefaults({
formatter: (error) => error.msg,
});
class Controller {
// ID parameter validation (/:id)
static id_validation = (req, res, next) => {
const post_id = req.params.id;
if (!ObjectID.isValid(post_id)) {
return res.redirect("/");
}
next();
};
// Post data validation
static validation = (req, res, next) => {
const errors = validation_result(req).mapped();
if (Object.keys(errors).length) {
res.locals.validation_errors = errors;
}
next();
};
// Show all posts
static all_posts = async (req, res, next) => {
try {
const all_posts = await Post.find()
.select("title")
.sort({ createdAt: -1 }); //Descending Order
if (all_posts.length === 0) {
return res.redirect("/create");
}
res.render("post-list", {
all_posts,
});
} catch (err) {
next(err);
}
};
// Create a new post page
static create_post_page = (req, res, next) => {
res.render("create-update-post");
};
// inserting new post into the database
static insert_post = async (req, res, next) => {
if (res.locals.validation_errors) {
return res.render("create-update-post", {
validation_errors: JSON.stringify(res.locals.validation_errors),
old_inputs: req.body,
});
}
try {
const { title, content, author } = matchedData(req);
let post = new Post({
title,
content,
author,
});
post = await post.save();
if (ObjectID.isValid(post.id)) {
return res.redirect(`/post/${post.id}`);
}
res.render("create-update-post", {
error: "Failed to insert post.",
});
} catch (err) {
next(err);
}
};
// Single post page to view a single post by the ID
static single_post_page = async (req, res, next) => {
try {
const the_post = await Post.findById(req.params.id);
if (the_post === null) return res.redirect("/");
res.render("single", {
post: the_post,
});
} catch (err) {
next(err);
}
};
// Edit page
static edit_post_page = async (req, res, next) => {
const the_post = await Post.findById(req.params.id).select(
"title content author"
);
if (the_post === null) return res.redirect("/");
res.render("create-update-post", {
edit: true,
old_inputs: the_post,
});
};
// update an existing post
static update_post = async (req, res, next) => {
if (res.locals.validation_errors) {
return res.render("create-update-post", {
validation_errors: JSON.stringify(res.locals.validation_errors),
old_inputs: req.body,
});
}
try {
const { title, content, author } = matchedData(req);
await Post.findByIdAndUpdate(req.params.id, {
title,
content,
author,
});
res.redirect(`/post/${req.params.id}`);
} catch (err) {
next(err);
}
};
// Delete a post
static delete_post = async (req, res, next) => {
try {
await Post.findByIdAndDelete(req.params.id);
res.redirect("/");
} catch (err) {
next(err);
}
};
}
export default Controller;
Itβs time to create views
All the views will be inside the views
folder. Here is a list of the views we have to create β create-update-post.ejs
, post-list.ejs
, and single.ejs
.
- create-update-post.ejs
- post-list.ejs
- single.ejs

<%
const isEdit = (typeof edit !== 'undefined') ? true : false;
const old_value = (field_name) => {
if(typeof old_inputs !== 'undefined'){
return old_inputs[field_name] ? old_inputs[field_name] : "";
}
return "";
}
%>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%- isEdit ? "Edit Post" : "Insert Post" %></title>
<link rel="stylesheet" href="/style.css" />
</head>
<body>
<div class="container">
<div class="wrapper">
<h1 class="title"><%- isEdit ? "Edit Post" : "Insert Post" %></h1>
<form action="" method="POST" class="form" novalidate>
<label for="p_title">
Title: <span class="er-msg title"></span>
</label>
<input type="text" name="title" id="p_title"
placeholder="Post title" value="<%- old_value("title"); %>"
required />
<label for="author_name">
Author: <span class="er-msg author"></span>
</label>
<input type="text" name="author" id="author_name"
placeholder="Author name" value="<%- old_value("author");
%>" required />
<label for="p_content">
Content: <span class="er-msg content"></span>
</label>
<textarea
name="content"
id="p_content"
placeholder="Your thought..."
required
><%- old_value("content"); %></textarea>
<% if(typeof error !== "undefined"){ %>
<p class="error"><%- error %></p>
<% } %>
<button type="submit"><%- isEdit ? "Update" : "Insert" %></button>
</form>
<div style="text-align: center; padding-top:20px;"><a href="/" class="action-btn">All posts</a></div>
</div>
</div>
<% if(typeof validation_errors !== "undefined"){ %>
<script>
let spanItem;
let item;
const errs = <%- validation_errors %>;
for (const property in errs) {
spanItem = document.querySelector(`.er-msg.${property}`);
item = document.querySelector(`[name="${property}"]`);
item.classList.add('err-input');
spanItem.innerText = errs[property];
}
</script>
<% } %>
</body>
</html>

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>All Posts</title>
<link rel="stylesheet" href="/style.css" />
</head>
<body>
<div class="container">
<div class="wrapper post-list">
<h1 class="title txt-left">π― All Posts</h1>
<a href="/create" class="add-new-btn">Insert a new post</a>
<table class="table">
<thead>
<tr>
<th>Title</th>
<th>Edit</th>
<th>Delete</th>
</tr>
</thead>
<tbody><% for(const post of all_posts){%>
<tr>
<td>
<a href="/post/<%- post.id %>" class="link"><%- post.title %></a>
</td>
<td>
<a class="action-btn" href="/edit/<%- post.id %>">Edit</a>
</td>
<td>
<a class="action-btn delete" href="/delete/<%- post.id %>">Delete</a>
</td>
</tr>
<%} %>
</tbody>
</table>
</div>
</div>
<script>
// Confirmation of Post Deletion
const deleteBtns = document.querySelectorAll("a.delete");
function delete_me(e){
e.preventDefault();
if(confirm("Do you want to delete this post?")){
location.href = e.target.getAttribute("href");
}
}
for(const btn of deleteBtns){
btn.onclick = delete_me;
}
</script>
</body>
</html>

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Single Post</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<div class="container">
<div class="wrapper" style="max-width: 600px;">
<h1 class="title txt-left"><%- post.title %></h1>
<p style="font-size: 17px;"><%- post.content %></p>
<ul style="padding-left: 18px; color:#777;">
<li><strong>Author:</strong> <%- post.author %></li>
<li><strong>Created At:</strong> <%- post.createdAt.toLocaleDateString() %> | <strong>Updated At:</strong> <%- post.updatedAt.toLocaleDateString() %></li>
</ul>
<div><a href="/" class="action-btn">All posts</a> | <a href="/edit/<%- post.id %>" class="action-btn">β Edit</a></div>
</div>
</div>
</body>
</html>
Stylesheet for the views
Here is the Stylesheet (style.css
) which will be created inside the public/
folder.
@import url("https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;700&display=swap");
*,
*::before,
*::after {
box-sizing: border-box;
}
:root {
line-height: 1.5;
font-weight: 400;
font-size: 16px;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
--font: "Ubuntu", sans-serif;
--color: #ff4137;
--color: #0095ff;
}
body {
padding: 50px;
margin: 0;
font-family: var(--font);
background: #f7f7f7;
color: #222;
}
input,
textarea,
button,
select {
font-family: var(--font);
font-size: 1rem;
}
.title {
text-align: center;
margin-top: 0;
}
.txt-left {
text-align: left;
}
.wrapper {
max-width: 500px;
margin: 0 auto;
background: white;
padding: 50px;
border-radius: 3px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -2px rgba(0, 0, 0, 0.1);
}
.wrapper.post-list {
max-width: 700px;
}
.form label {
font-weight: 700;
margin-top: 10px;
display: inline-block;
}
.form label:first-child {
margin: 0;
}
.form button,
.form input,
.form textarea {
width: 100%;
padding: 10px;
outline: none;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 3px;
}
.form button:is(:hover, :focus),
.form input:is(:hover, :focus),
.form textarea:is(:hover, :focus) {
border-color: var(--color);
}
.form textarea {
resize: vertical;
min-height: 80px;
}
.form button {
margin-top: 15px;
cursor: pointer;
background: var(--color);
color: white;
padding: 13px;
border-color: var(--color);
box-shadow: rgba(0, 0, 0, 0.1) 0 4px 12px;
transition: all 250ms;
}
.form button:hover {
transform: translateY(-3px);
}
.form button:active {
box-shadow: rgba(0, 0, 0, 0.06) 0 2px 4px;
transform: translateY(0);
}
.table {
border-collapse: collapse;
width: 100%;
border-radius: 5px;
overflow: hidden;
border: 1px solid transparent;
}
.table tr,
.table th,
.table td {
border: 1px solid rgba(0, 0, 0, 0.2);
}
.table th {
font-size: 1.2rem;
color: #444;
}
.table th,
.table td {
padding: 7px;
}
.table td {
text-align: center;
}
a {
cursor: pointer;
text-decoration: none;
outline: none;
}
.link {
color: #222;
}
.link:hover {
text-decoration: underline;
color: var(--color);
}
.action-btn {
background-color: #fff;
border: 1px solid #dbdbdb;
border-radius: 0.375em;
box-shadow: none;
color: #363636;
font-size: 14px;
display: inline-flex;
padding: calc(0.3em - 1px) 0.7em;
}
.action-btn:hover {
border-color: #b5b5b5;
}
.action-btn:focus:not(:active) {
box-shadow: rgba(72, 95, 199, 0.25) 0 0 0 0.125em;
}
.action-btn.delete {
color: #ff4137;
border-color: #ff5836;
}
.add-new-btn {
background-color: var(--color);
border: 1px solid transparent;
border-radius: 3px;
box-shadow: rgba(255, 255, 255, 0.4) 0 1px 0 0 inset;
display: inline-block;
color: white;
cursor: pointer;
padding: 8px 0.8em;
user-select: none;
}
.add-new-btn:focus {
background-color: #07c;
}
.add-new-btn:active {
background-color: #0064bd;
box-shadow: none;
}
.error,
.er-msg {
color: #ff4137;
}
.error,
.err-input {
border: 1px solid #ff4137 !important;
}
.error {
padding: 10px;
border-radius: 3px;
}
Now start the app and test it…
Thank You.. ππ»β€οΈβ€οΈ