Node JS + MongoDB CRUD Application

How to Make Node JS + MongoDB CRUD Application?

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.

add type module to the node mongodb CRUD app package.json file

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.

node mongodb crud app folder structure

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

Insert a new post
<% 
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>
show all posts
<!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>
single post
<!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.. πŸ™πŸ»β€οΈβ€οΈ