How to Make CRUD Application Using Node JS + MongoDB

In this step-by-step tutorial, you will learn how to create a simple CRUD application using Node.js with Express.js framework and MongoDB as the database.

Step 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.

mkdir node-mongo-crud-app
cd node-mongo-crud-app

After that, run npm init -y on your terminal to initialize the npm into this folder:

npm init -y

Step 2: Install the Necessary Node Packages

Install the following Node packages:

  • express
  • express-validator: for form validation.
  • ejs: As the template engine.
  • mongoose: Object Modeling tool for MongoDB.
npm i express express-validator ejs mongoose

Step 3: Enable Es6 Import for this CRUD App Project

In this project, we will use es6 import, so you have to add "type": "module" in the package.json. Like the following image:

enable es6  import in node by adding type module

Step 4: Application Folder Structure

We have to create the Files and folders listed in the following image to build this Node JS MongoDB CRUD Application.

node mongo crud app folder structure

Step 5: “PostModel.js” Model for Posts Collection

This app will perform CRUD operations with posts like – Create Post, Read Post, Update Post and Delete Post.

In this project we will use the 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. Here is the PostModel.js which will goes at the root of the folder:

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;

Step 6: Create Routes and Controllers (routes.js, Controller.js)

Now create routes.js and Controller.js at the root of the folder.

routes.js: contains all the routes with validation rules.

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;

Controller.js: The Controller class contains all the callbacks that handle all the requests and perform all CRUD operations.

// Import necessary modules and models
import { validationResult, matchedData } from "express-validator";
import Post from "./PostModel.js";
import { Types } from "mongoose";
const ObjectID = Types.ObjectId; // to check valid ObjectID

// Set default validation result formatter
const validation_result = validationResult.withDefaults({
    formatter: (error) => error.msg,
});

// Controller class with methods for handling routes
class Controller {
    // Middleware for validating ID parameter
    static id_validation = (req, res, next) => {
        const post_id = req.params.id;
        if (!ObjectID.isValid(post_id)) {
            return res.redirect("/");
        }
        next();
    };

    // Middleware for validating post data
    static validation = (req, res, next) => {
        const errors = validation_result(req).mapped();
        if (Object.keys(errors).length) {
            res.locals.validation_errors = errors;
        }
        next();
    };

    // Route handler to display 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);
        }
    };

    // Route handler to display create post page
    static create_post_page = (req, res, next) => {
        res.render("create-update-post");
    };

    // Route handler to insert a 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);
        }
    };

    // Route handler to display single post page
    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);
        }
    };

    // Route handler to display edit post 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,
        });
    };

    // Route handler to 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);
        }
    };

    // Route handler to 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;

Step 7: Create Views for Frontend

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.

  1. create-update-post.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>
html form for create and update post
  1. post-list.ejs
<!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>
Read all posts
  1. single.ejs
<!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>
Node CRUD app reading single post by id

Step 8: Create “style.css” for the Views

Here is the CSS code for the style.css file 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;
}

Step 9: “index.js” Root of this Node Mongo CRUD Application

import express from "express";
import mongoose from "mongoose";
import path, { dirname } from "path";
import { fileURLToPath } from "url";
import routes from "./routes.js";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const DB_NAME = "test"; // Database Name
const DB_URL = `mongodb://127.0.0.1:27017/${DB_NAME}`;

const app = express();

app.set("view engine", "ejs");
app.set("views", path.join(__dirname, "views"));

app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, "public")));

app.use(routes);

// Database Connection
mongoose
    .connect(DB_URL)
    .then(() => {
        // Run the application if DB is connected successfully
        app.listen(3000, () => {
            console.log("Server is Running on PORT 3000");
        });
    })
    .catch((err) => {
        console.log(err);
    });

Step 10: Test this Node MongoDB CRUD Application

Start the application node index.js and open http://localhost:3000 on your browser and test this node express mongo CRUD application.

Leave a Reply

Your email address will not be published. Required fields are marked *

We use cookies to ensure that we give you the best experience on our website. Privacy Policy