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