13 min read· by Awab Tech Lover

Node.js Express MongoDB CRUD Tutorial: Build a REST API

Learn to build a simple Node.js Express MongoDB CRUD API from scratch with this step-by-step tutorial. Master RESTful principles and database integration.

Node.js Express MongoDB CRUD Tutorial: Build a REST API

Node.js Express MongoDB CRUD Tutorial: Build a Simple REST API

Welcome to this comprehensive Node.js Express MongoDB CRUD tutorial, where we'll guide you through building a basic yet fully functional RESTful API from the ground up. If you're looking to understand how to create, read, update, and delete (CRUD) data using a robust tech stack, you've come to the right place. This guide is designed to be practical, example-driven, and empowers you with the knowledge to kickstart your backend development journey.

We'll leverage three powerful technologies:

  • Node.js: A JavaScript runtime for server-side development.
  • Express.js: A fast, unopinionated, minimalist web framework for Node.js.
  • MongoDB: A popular NoSQL database for flexible data storage.

By the end of this tutorial, you'll have a working API that can manage a collection of resources, demonstrating core CRUD operations.

Prerequisites

Before we dive into the code, ensure you have the following installed on your system:

  • Node.js and npm: Download from nodejs.org. npm (Node Package Manager) comes bundled with Node.js.
  • MongoDB: Install MongoDB Community Server from mongodb.com. You can also use MongoDB Atlas for a cloud-hosted solution.
  • A code editor: Visual Studio Code is highly recommended.
  • Postman or Insomnia: API clients for testing your endpoints.

Setting Up Your Project

Let's begin by creating a new Node.js project.

1. Initialize Your Project Directory

Create a new folder for your project and navigate into it using your terminal.

mkdir nodejs-crud-api
cd nodejs-crud-api

2. Initialize npm

This command creates a package.json file, which manages your project's dependencies and scripts.

npm init -y

The -y flag accepts all default options. You can omit it if you prefer to fill in details manually.

3. Install Dependencies

We'll need express for our web server and mongoose to interact with MongoDB. dotenv is useful for managing environment variables.

npm install express mongoose dotenv
  • express: The core web framework.
  • mongoose: An ODM (Object Data Modeling) library for MongoDB, providing a schema-based solution to model application data.
  • dotenv: Loads environment variables from a .env file into process.env.

Connecting to MongoDB

Before building our API, we need to establish a connection to our MongoDB database.

1. Configure Environment Variables

Create a file named .env in the root of your project. This file will store sensitive information like your database connection string.

PORT=3000
MONGODB_URI=mongodb://localhost:27017/mydatabase
  • PORT: The port your Express server will listen on.
  • MONGODB_URI: Your MongoDB connection string. If you're running MongoDB locally with default settings, mongodb://localhost:27017/mydatabase is a common format, where mydatabase is the name of your database. For MongoDB Atlas, your URI will be provided by Atlas.

2. Create the Database Connection File

Create a folder named config and inside it, a file db.js. This file will handle the MongoDB connection logic.

// config/db.js
require('dotenv').config();
const mongoose = require('mongoose');

const connectDB = async () => {
  try {
    const conn = await mongoose.connect(process.env.MONGODB_URI, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
      // useFindAndModify: false, // Deprecated in Mongoose 6+
      // useCreateIndex: true,    // Deprecated in Mongoose 6+
    });

    console.log(`MongoDB Connected: ${conn.connection.host}`);
  } catch (error) {
    console.error(`Error: ${error.message}`);
    process.exit(1); // Exit process with failure
  }
};

module.exports = connectDB;

Here's what this code does:

  • require('dotenv').config(): Loads environment variables from .env.
  • mongoose.connect(): Establishes the connection to MongoDB. The options useNewUrlParser and useUnifiedTopology are recommended for modern MongoDB drivers.

Creating Your Express Server

Now, let's set up the basic Express server.

1. Create server.js (or app.js)

In the root of your project, create server.js.

// server.js
require('dotenv').config();
const express = require('express');
const connectDB = require('./config/db');

const app = express();
const PORT = process.env.PORT || 5000;

// Connect to database
connectDB();

// Middleware to parse JSON bodies
app.use(express.json());

// Basic route
app.get('/', (req, res) => {
  res.send('API is running...');
});

// Start the server
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

To run your server, add a script to your package.json:

"scripts": {
  "start": "node server.js"
}

Now, run npm start in your terminal. You should see "MongoDB Connected: [your_host]" and "Server running on port 3000" (or 5000 if PORT isn't set in .env). Visit http://localhost:3000 in your browser, and you should see "API is running...".

Defining Your Data Model (Mongoose Schema)

For this tutorial, let's create a simple API for managing "products".

1. Create a models Directory

Create a new directory called models and inside it, a file named Product.js.

// models/Product.js
const mongoose = require('mongoose');

const productSchema = mongoose.Schema({
  name: {
    type: String,
    required: [true, 'Please add a product name'],
    unique: true, // Product names should be unique
  },
  description: {
    type: String,
    required: [true, 'Please add a description'],
  },
  price: {
    type: Number,
    required: [true, 'Please add a price'],
    min: 0, // Price cannot be negative
  },
  quantity: {
    type: Number,
    required: [true, 'Please add a quantity'],
    min: 0, // Quantity cannot be negative
    default: 0,
  },
  category: {
    type: String,
    required: false, // Category is optional
  },
  createdAt: {
    type: Date,
    default: Date.now,
  },
});

module.exports = mongoose.model('Product', productSchema);

In this schema:

  • We define the structure of our Product documents.
  • Each field has a type (e.g., String, Number, Date).
  • required ensures the field must be present.
  • unique ensures no two products have the same name.
  • min sets a minimum value for numbers.
  • default provides a default value if not specified.
  • mongoose.model('Product', productSchema) compiles our schema into a model. 'Product' will be the name of the collection in MongoDB (it will be pluralized to products).

Building the CRUD API Endpoints (Controllers & Routes)

Now for the core of our Node.js Express MongoDB CRUD tutorial: implementing the API endpoints. We'll separate our routes and controller logic for better organization.

1. Create Controllers

Create a directory controllers and inside it, productController.js. This file will contain the logic for handling requests.

// controllers/productController.js
const Product = require('../models/Product');

// @desc    Get all products
// @route   GET /api/products
// @access  Public
const getProducts = async (req, res) => {
  try {
    const products = await Product.find({});
    res.status(200).json(products);
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
};

// @desc    Get single product by ID
// @route   GET /api/products/:id
// @access  Public
const getProductById = async (req, res) => {
  try {
    const product = await Product.findById(req.params.id);
    if (!product) {
      return res.status(404).json({ message: 'Product not found' });
    }
    res.status(200).json(product);
  } catch (error) {
    // Handle invalid ObjectId format
    if (error.name === 'CastError') {
      return res.status(400).json({ message: 'Invalid product ID format' });
    }
    res.status(500).json({ message: error.message });
  }
};

// @desc    Create a product
// @route   POST /api/products
// @access  Public (for this tutorial, generally requires authentication)
const createProduct = async (req, res) => {
  try {
    const { name, description, price, quantity, category } = req.body;

    // Basic validation
    if (!name || !description || !price || !quantity) {
      return res.status(400).json({ message: 'Please enter all required fields: name, description, price, quantity' });
    }

    const product = new Product({
      name,
      description,
      price,
      quantity,
      category,
    });

    const createdProduct = await product.save();
    res.status(201).json(createdProduct);
  } catch (error) {
    // Handle unique field errors (e.g., duplicate product name)
    if (error.code === 11000) {
      return res.status(400).json({ message: 'Product with this name already exists' });
    }
    res.status(500).json({ message: error.message });
  }
};

// @desc    Update a product by ID
// @route   PUT /api/products/:id
// @access  Public (generally requires authentication)
const updateProduct = async (req, res) => {
  try {
    const { name, description, price, quantity, category } = req.body;
    const { id } = req.params;

    const product = await Product.findById(id);

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

    // Update product fields
    product.name = name || product.name;
    product.description = description || product.description;
    product.price = price || product.price;
    product.quantity = quantity || product.quantity;
    product.category = category || product.category;

    const updatedProduct = await product.save();
    res.status(200).json(updatedProduct);
  } catch (error) {
    if (error.name === 'CastError') {
      return res.status(400).json({ message: 'Invalid product ID format' });
    }
    if (error.code === 11000) {
      return res.status(400).json({ message: 'Product with this name already exists' });
    }
    res.status(500).json({ message: error.message });
  }
};

// @desc    Delete a product by ID
// @route   DELETE /api/products/:id
// @access  Public (generally requires authentication)
const deleteProduct = async (req, res) => {
  try {
    const { id } = req.params;

    const product = await Product.findByIdAndDelete(id); // findByIdAndRemove is deprecated

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

    res.status(200).json({ message: 'Product removed' });
  } catch (error) {
    if (error.name === 'CastError') {
      return res.status(400).json({ message: 'Invalid product ID format' });
    }
    res.status(500).json({ message: error.message });
  }
};

module.exports = {
  getProducts,
  getProductById,
  createProduct,
  updateProduct,
  deleteProduct,
};

Each function in productController.js corresponds to a specific CRUD operation:

  • getProducts: Retrieves all products.
  • getProductById: Retrieves a single product by its ID.
  • createProduct: Adds a new product to the database.
  • updateProduct: Modifies an existing product.
  • deleteProduct: Removes a product.

We use try...catch blocks to handle potential errors and return appropriate HTTP status codes and messages.

2. Create Routes

Create a directory routes and inside it, productRoutes.js. This file will define the API endpoints and link them to our controller functions.

// routes/productRoutes.js
const express = require('express');
const router = express.Router();
const {
  getProducts,
  getProductById,
  createProduct,
  updateProduct,
  deleteProduct,
} = require('../controllers/productController');

// Define routes for products
router.route('/')
  .get(getProducts)    // GET /api/products
  .post(createProduct); // POST /api/products

router.route('/:id')
  .get(getProductById)    // GET /api/products/:id
  .put(updateProduct)     // PUT /api/products/:id
  .delete(deleteProduct); // DELETE /api/products/:id

module.exports = router;

This uses Express's router to group related routes. router.route('/') allows chaining different HTTP methods to the same path.

3. Integrate Routes into server.js

Finally, we need to tell our Express app to use these routes. Update server.js:

// server.js
require('dotenv').config();
const express = require('express');
const connectDB = require('./config/db');
const productRoutes = require('./routes/productRoutes'); // Import product routes

const app = express();
const PORT = process.env.PORT || 5000;

// Connect to database
connectDB();

// Middleware to parse JSON bodies
app.use(express.json());

// Basic route (optional, can be removed once API routes are active)
// app.get('/', (req, res) => {
//   res.send('API is running...');
// });

// Product routes
app.use('/api/products', productRoutes); // Use product routes for /api/products path

// Start the server
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Restart your server (Ctrl+C then npm start).

Testing Your CRUD API

Now it's time to test the fruits of our Node.js Express MongoDB CRUD tutorial. We'll use Postman (or Insomnia) to interact with our API.

1. Create a Product (POST)

  • Method: POST
  • URL: http://localhost:3000/api/products
  • Headers: Content-Type: application/json
  • Body: raw, JSON
{
  "name": "Laptop Pro 2023",
  "description": "Powerful laptop for professionals",
  "price": 1499.99,
  "quantity": 10,
  "category": "Electronics"
}

Send the request. You should get a 201 Created status with the newly created product details, including its _id.

2. Get All Products (GET)

  • Method: GET
  • URL: http://localhost:3000/api/products

Send the request. You should get a 200 OK status with an array containing the product you just created.

3. Get a Single Product (GET by ID)

Copy the _id from the product created in step 1.

  • Method: GET
  • URL: http://localhost:3000/api/products/YOUR_PRODUCT_ID (replace YOUR_PRODUCT_ID)

Send the request. You should get a 200 OK status with the details of that specific product. If you use an invalid ID format or a non-existent ID, you'll receive 400 or 404 respectively.

4. Update a Product (PUT)

Copy the _id again.

  • Method: PUT
  • URL: http://localhost:3000/api/products/YOUR_PRODUCT_ID
  • Headers: Content-Type: application/json
  • Body: raw, JSON
{
  "name": "Laptop Pro 2023 (Updated)",
  "price": 1399.99,
  "quantity": 8
}

Note: You don't have to send all fields; only the ones you want to update.

Send the request. You should get a 200 OK status with the updated product details.

5. Delete a Product (DELETE)

Copy the _id.

  • Method: DELETE
  • URL: http://localhost:3000/api/products/YOUR_PRODUCT_ID

Send the request. You should get a 200 OK status with a message Product removed. If you try to GET this product again, you'll receive a 404 Not Found.

Summary of API Endpoints

Here's a quick overview of the API endpoints we've created:

Method Endpoint Description Request Body Examples
GET /api/products Get all products (None)
GET /api/products/:id Get product by ID (None)
POST /api/products Create a new product { "name": "...", "description": "...", "price": ..., "quantity": ... }
PUT /api/products/:id Update product by ID { "name": "...", "price": ... } (partial update)
DELETE /api/products/:id Delete product by ID (None)

Next Steps and Enhancements

This Node.js Express MongoDB CRUD tutorial provides a solid foundation. Here are some ideas for further enhancing your API:

  • Authentication & Authorization: Implement JWT (JSON Web Tokens) for securing your API endpoints, ensuring only authorized users can perform certain actions (e.g., only admins can create/update/delete products).
  • Validation: Use a library like express-validator for more robust input validation.
  • Error Handling Middleware: Create custom error handling middleware to centralize and improve error responses.
  • Pagination, Sorting, Filtering: Add query parameters to your GET /api/products endpoint to allow clients to paginate results, sort them by fields, and filter based on criteria.
  • CORS: Implement Cross-Origin Resource Sharing if your frontend is hosted on a different domain. Use the cors npm package.
  • Deployment: Deploy your API to platforms like Heroku, Vercel, or AWS.
  • Testing: Write unit and integration tests for your API using frameworks like Jest or Mocha.
  • Logging: Add a logging library like Winston or Morgan to log requests and errors.

Conclusion

Congratulations! You've successfully built a simple yet robust RESTful CRUD API using Node.js, Express, and MongoDB. This Node.js Express MongoDB CRUD tutorial has covered project setup, database connection, schema definition, and implementing all four CRUD operations. This architecture is highly scalable and forms the backbone of many modern web applications. Keep experimenting, keep building, and happy coding!

FAQ

Q1: What does CRUD stand for in the context of an API?

A1: CRUD is an acronym for Create, Read, Update, and Delete. These are the four fundamental operations that can be performed on data within a database or through an API. They map directly to HTTP methods: POST (Create), GET (Read), PUT/PATCH (Update), and DELETE (Delete).

Q2: Why use Mongoose instead of directly interacting with MongoDB?

A2: Mongoose is an Object Data Modeling (ODM) library for MongoDB and Node.js. It provides a schema-based solution to model your application data, enforcing structure and validation. It simplifies interactions with MongoDB by giving you an object-oriented way to work with your data, including features like query building, middleware, and type casting, reducing boilerplate code.

Q3: What is the purpose of express.json() middleware?

A3: The express.json() middleware is used to parse incoming request bodies with JSON payloads. When a client sends a POST or PUT request with a Content-Type: application/json header, this middleware parses the JSON string into a JavaScript object and attaches it to the req.body property, making it accessible in your route handlers.

Q4: How can I handle validation errors more gracefully than in this tutorial?

A4: In a production application, you would typically use a dedicated validation library like express-validator or Joi. These libraries allow you to define detailed validation rules for your request bodies and query parameters, providing more specific error messages and handling multiple validation failures efficiently. You can also implement custom error handling middleware to catch and format these errors consistently.

Q5: Is it safe to expose my MongoDB connection string directly in .env?

A5: Yes, it is safe to put your MongoDB connection string in a .env file as long as the .env file is never committed to your version control system (e.g., Git). .env files are intended for local development. For deployment, you would configure environment variables directly on your hosting platform, which keeps them secure and separate from your codebase. Always add .env to your .gitignore file.

Q6: What's the difference between PUT and PATCH for updating resources?

A6: Both PUT and PATCH are used to update existing resources. A PUT request is typically used for full replacement of a resource; the request body should contain the complete new state of the resource. If fields are omitted, they might be set to null or default values. A PATCH request, on the other hand, is for partial updates; the request body should only contain the fields that need to be modified, leaving other fields untouched. This tutorial uses PUT for simplicity, allowing partial updates, which is a common practice in many APIs.