MissionaryZeal

Building a Robust API with Node.js, Express, and MongoDB Using MVC Pattern

This post is going to teach you how to create an API to grasp into web development field. Node.js, with its non-blocking I/O model, combined with Express.js, a minimalist web framework, offers a powerful foundation for building APIs. MongoDB, a NoSQL database, provides flexibility and scalability to handle varying data structures. By adhering to the Model-View-Controller (MVC) architectural pattern, we can efficiently organize their codebase, enhancing maintainability and scalability. In this guide, we’ll walk through the process of creating a RESTful API using Node.js, Express.js, and MongoDB, following the MVC pattern to structure our application effectively. Let’s dive in!

In the very first step, let’s see how the MVC folder structure looks like.

Prerequisites

  • Node.js
  • MongoDB install or Live MongoDB Cluster account

Getting Started

We need to create a blank folder and then initialize a npm package

$ mkdir api-node-epxress
$ cd api-node-epxress
$ npm init -y

Note: -y command stands for “yes.” When you run npm init -y, it initializes a new package.json file for your Node.js project with default values for all fields. This means that you’re essentially accepting all default configurations without being prompted for confirmation.

Now, let’s install some useful npm package.

$ npm install express mongoose dotenv

Basic Express serve setup

Create a server.js file in the root directory of the project.

const express = require('express');
const app = express();
const port = 4000;
app.listen(port, () => { console.log(`The app running port ${port}`); })

Let’s try to spin up our server. Open the terminal in the project’s folder.

$ node server.js
The app running port 4000

Create Routes

Now create a routes folder, and inside the folder, create a product.js router file for our application routes.

const express = require("express");
const router = express.Router();
const { allProducts, createProduct, editProduct, updateProduct, deleteProduct } = require("../controllers/ProductController");
router.get('/', allProducts)
router.post('/', createProduct)
router.get('/:id', editProduct)
router.put('/:id', updateProduct)
router.delete('/:id', deleteProduct)

module.exports = router

We have done here common API requests with different only method names like get, post, put, and delete. Let’s suppose we have api/products/ path. The path we will write later in the serve.js file. We also separated the functionality from the controller and imported those methods from the controller.

Build Endpoint

Now create our endpoint for testing our APIs. You can use any API testing software. We use postman. Make some changes in server.js file to call the API endpoint.

const express = require('express');
const app = express();
const products = require('./routes/products');

require("dotenv").config();
const port = process.env.PORT || 4000;

app.use(express.json());


app.get('/', (req, res) => {
    res.send("Node App");
});

app.use('/api/products', products);



app.listen(port, () => { console.log(`The app running port ${port}`); })

We imported and used here the product routes. We also added api prefix in the endpoint.

our API endpoints look like. localhost:4000/api/products. Now we can test our all endpoints just change HTTP method and passing parameter. Below see all http methods.

Method       Endpoint              Definition
get       api/products          Get all products
post      api/products          Create a product
get       api/products/id       Show a product
put       api/products/id       Update a product
delete    api/products/id       Delete a product

Now we can test our product using these methods and endpoints.

Create controller

Let’s separate the controller from our application’s business logic. Create a ProductController file inside the controller folder.

/** Return all products
 * @api GET /api/products
 * @returns JSON
 */
const allProducts = async (req, res) => {
  res.json("all products");
}


/** Create a product
 * @api POST /api/products
 * @returns JSON
 */
const createProduct = async (req, res, next) => {

     res.json("create product");
}

/** Show product
 * @api GET /api/products/:id
 * @returns JSON
 */
const showProduct = async (req, res) => {
    const { id } = req.params;

    res.json("show product " + id);
   
}


/** Update  product
 * @api PUT /api/products/:id
 * @returns JSON
 */
const updateProduct = async (req, res) => {

        const { id } = req.params;

        res.json("update product " + id);
       
}


/** Delete  product
 * @api PUT /api/products/:id
 * @returns JSON
 */
const deleteProduct = async (req, res) => {
  const { id } = req.params;
    res.json("delete product " + id);
}


module.exports = {
    allProducts,
    createProduct,
    editProduct,
    updateProduct,
    deleteProduct
}

Here, we created a controller with curd method then export the methods. Later we will do all database operations in this controller.

Connect with MongoDB

Create config folder inside the folder create db.js file to establish our connection with MongoDB online server. You can create your database here https://cloud.mongodb.com/

const mongoose = require('mongoose');

const connectDB = async () => {

    try {
        const conn = await mongoose.connect(process.env.MONGO_URI);
        console.log(`Connection successfully stablish on host ${conn.connection.host}`);
    } catch (error) {
        console.log(`Error ${error.message}`);
        process.exit(1);
    }

}

module.exports = connectDB

We imported the mongoose and built connection with online mongoDB server. Your MONGO_URI should come from .env file. You can get the MONGO_URI after create an account and create a project in mongoDB server.

Create Model

Let’s create first model folder on root directory and then create schema file for our model using mongoose. In our case this is Prodouct.js file.

const mongoose = require('mongoose');

const productSchema = new mongoose.Schema({
    name: {
        type: String,
        required: [true, "Product name is required"],
        unique: [true, "Product name must be unique"],
        minlength: [3, "Product name at least 3 character"],
        maxlength: [30, "Product name cannot be more than 30 character"],
        trim: true,
    },
    slug: {
        type: String
    },
    description: {
        type: String,
    },
    price: {
        type: Number,
        required: [true, "Price is required"],
    }

});

const Product = mongoose.model('Product', productSchema);

module.exports = Product;

We created a schema name it productSchema and we will be created a collection in our database call Product. Then defines the shape of the documents within that collection.

CRUD Operation

Let’s going complete crud in operation in the ProductController


const Product = require("../models/Product");

/** Return all products
 * @api GET /api/products
 * @returns JSON
 */
const allProducts = async (req, res) => {
    const products = await Product.find();
    res.status(200).json(products);
}


/** Create a product
 * @api POST /api/products
 * @returns JSON
 */
const createProduct = async (req, res, next) => {

    try {
        const product = await Product.create(req.body);
        res.status(200).json({ message: "Product Created Successfully", product });
    } catch (error) {
        console.log("Error " + error);
    }

}

/** Show product
 * @api GET /api/products/:id
 * @returns JSON
 */
const showProduct = async (req, res) => {
    const { id } = req.params;

    try {
        const product = await Product.findOne({ _id: id });

        if (!product) {
            return res.status(404).json({ error: "Product not found" });
        }

        res.status(200).json({ message: "Edit Product", product });
    } catch (error) {
        console.error("Error editing product:", error);
        res.status(500).json({ error: "Internal server error" });
    }
}


/** Update  product
 * @api PUT /api/products/:id
 * @returns JSON
 */
const updateProduct = async (req, res) => {
    try {
        const { id } = req.params;
        const product = await Product.findOneAndUpdate({ _id: id }, req.body, {
            new: true,
            runValidators: true
        });

        if (!product) {
            return res.status(404).json({ error: "Product not found" });
        }

        res.status(200).json({ message: "Product Updated Successfully", product });
    } catch (error) {
        console.error("Error updating product:", error);
        res.status(500).json({ error: "Internal server error" });
    }
}


/** Delete  product
 * @api PUT /api/products/:id
 * @returns JSON
 */
const deleteProduct = async (req, res) => {
    try {
        const { id } = req.params;
        const product = await Product.findOneAndDelete({ _id: id });
        if (!product) {
            return res.status(404).json({ error: "Product not found" });
        }

        res.status(200).json({ message: "Product Deleted Successfully", product });
    } catch (error) {
        console.error("Error delete product:", error);
        res.status(500).json({ error: "Internal server error" });
    }
}


module.exports = {
    allProducts,
    createProduct,
    editProduct,
    updateProduct,
    deleteProduct
}

Above piece of code we have written get all, create, show, update and delete product(crud) with mongoDB with the help of mongoose model.

Not Found Error Handling

First create a not found response for unknown endpoint. Create a middleware folder directory. inside the middleware folder create not-found.js file

const notFound = (req, res) => {
    res.status(404).send("Not found the route");
}
module.exports = notFound

We have created a not found middleware. When users hit unknown endpoint we sent this response.

We have to use this middleware in the server.js file.

const express = require('express');
const app = express();
const products = require('./routes/products');
const notFound = require('./middleware/not-found');  
require("dotenv").config();
const port = process.env.PORT || 4000;
const connectDB = require('./config/db');
connectDB();

app.use(express.json());



app.get('/', (req, res) => {
    res.send("Node App");
});

app.use('/api/products', products);

app.use(notFound);


app.listen(port, () => { console.log(`The app running port ${port}`);

We imported and used the not found middleware. Remember must use notFound middleware after actual route otherwise it is not working.

Custom Validation Error Handling

We already implemented several validations for the name and price fields in Product model using Mongoose schema. Go to Product.js model and check.

Let’s create a errors folder for handling those errors. inside the errors folder we have created a custom-error.js file.

class CustomApiError extends Error {
    constructor(message, statusCode) {
        super(message);
        this.message = message;
        this.statusCode = statusCode;
    }
}

module.exports = CustomApiError

We extended the Express Error class. and received the error message and status code inside the constructor. If you not familiar with javaScript classes we highly recommended to check OOPs concept of javaScript.

Let’s create one more file inside the errors folder mongo-error.js

const CustomApiError = require('./custom-error');

const handleMongoError = (error) => {
    let statusCode;
    let validationErrors = {};
    if (error.name === 'ValidationError') {
        statusCode = 400
        Object.keys(error.errors).forEach((field) => {
            validationErrors[field] = error.errors[field].message;
        });
    } else if (error.code === 11000) {
        statusCode = 400
        validationErrors['product'] = "Product name must be unique";

    } else {
        statusCode = 500
        validationErrors['error'] = "Internal Server Error";
    }

    return new CustomApiError(validationErrors, statusCode);
}

module.exports = handleMongoError

Here we handled all the mongoose errors. We imported the CustomApiError class file and passed error message and status code.

Go back to ProductController.js send error that getting when a Product creates

const handleMongoError = require("../errors/mongo-error");

/** Create a product
 * @api POST /api/products
 * @returns JSON
 */
const createProduct = async (req, res, next) => {

    try {
        const product = await Product.create(req.body);
        res.status(200).json({ message: "Product Created Successfully", product });
    } catch (error) {
        console.log("Error " + error);
        next(handleMongoError(error));
    }

}

We imported and used handleMongoError error file to pass error also used the next express built-in middleware.

next() function to call the next middleware function if the response of the current middleware is not terminated

Lastly we have to create a middleware for this custom-error when hit the create endpoint. Let’s create a file error-handling.js inside the middleware folder.

const CustomApiError = require("../errors/custom-error");

const errorHandling = (err, req, res, next) => {
    if (err instanceof CustomApiError) {
        res.status(err.statusCode).json(err.message);
    } else {
        res.status(500).json({ message: err.message });
    }
}
module.exports = errorHandling

This code we checked instance of the error class then send actual error message and status code.

Now use the middleware in the server.js file

const express = require('express');
const app = express();
const products = require('./routes/products');
const notFound = require('./middleware/not-found');
const errorHandling = require('./middleware/error-handling');
require("dotenv").config();
const port = process.env.PORT || 4000;
const connectDB = require('./config/db');
connectDB();

app.use(express.json());



app.get('/', (req, res) => {
    res.send("Node App");
});

app.use('/api/products', products);

app.use(notFound);
app.use(errorHandling);


app.listen(port, () => { console.log(`The app running port ${port}`); })

Having successfully implemented the API using the MVC pattern, we have deployed and tested it in my local environment. However, should you encounter any issues or have feedback for improvement, please don’t hesitate to share. Thank you for the opportunity to develop this solution, and I look forward to any suggestions you may have.

Please Check our API Error Handling and Validation in Node.js, Express, and Mongoose

Leave a Comment

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

Scroll to Top