How to Build a simple REST API with Node, Express and MongoDB

How to Build a simple REST API with Node, Express and MongoDB

·

12 min read

APIs have always been an important component of modern technology. Whenever we interact with a computer or system to retrieve information or perform a function, Application Programming Interfaces (APIs) are what help us communicate what we want to the system so it can understand and fulfill those requests. Among these, REST APIs are a popular choice because they are simple to use and can handle a lot of different tasks.

In this article we will be building a simple todo list REST API for managing tasks using Express and MongoDB. You’ll cover everything from setting up your project to building the endpoints that handle CRUD (Create, Read, Update, Delete) operations, and by the end you’ll have the foundational skills to create your own APIs with Express and MongoDB.

Prerequisites

Before you begin, ensure you have:

  1. Node.js Installed: Make sure Node.js is installed on your computer. You can download it from nodejs.org.

  2. MongoDB Setup: Either install MongoDB locally and access it with the GUI MongoDB Compass or create a free account on MongoDB Atlas for a cloud-based database.

  3. API Testing Tool: Install Postman, Insomnia, or Thunder Client to test the endpoints.

Setting Up Your Project

Start by creating a folder with any name you like. For this guide, I’ll advise you to use rest-api to keep things consistent. After that, open it in your preferred IDE. If you’re unsure, Visual Studio Code is a great choice.

Open the terminal in your VS Code or any IDE of your choice, navigate to the folder, and run the following command:

npm init -y

This will create a package.json file with default settings to manage your project dependencies.

Next, install the following packages by running the command:

npm install express mongoose nodemon dotenv

You’ll be making use of the packages:

  • express: For building the server.

  • mongoose: To interact with MongoDB.

  • nodemon: For automatically restarting the server during development.

  • dotenv: To manage environment variables securely.

To make development easier and prevent manually restarting the server after every change, add a start script in your package.json

"scripts": { 
    "start": "nodemon index.js" 
 }

This allows you to start your server with the command npm start . Your package.json file should look like this:

{
  "name": "rest-api",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "dotenv": "^16.4.7",
    "express": "^4.21.2",
    "mongoose": "^8.9.4",
    "nodemon": "^3.1.9"
  }
}

Connecting to MongoDB

Now that your project is all set up, the next step is to connect to your database, MongoDB.

Start by creating a .env file at the root of your folder. This is where you’ll store your MongoDB connection URI. This way, you can keep credentials safe and easily configurable.

Inside the .env file, store your connection URI in a variable, like this:

MONGO_URI=your_mongodb_connection_string

If you are using MongoDB Atlas, you can get the connection string from your Atlas dashboard. For local MongoDB with MongoDB Compass, it might look like this:

MONGO_URI=mongodb://localhost:27017/rest-api

Now, create a new file at the root of your project called db.js to manage the MongoDB connection.

Inside db.js, add the following code:

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

require('dotenv').config(); // Load environment variables

const connectDB = async () => {
  try {
    await mongoose.connect(process.env.MONGO_URI);
    console.log('MongoDB connected');
  } catch (error) {
    // Catch error if connection fails
    console.error('Error connecting to MongoDB:', error.message);
  }
};

module.exports = connectDB;

This is a simple function for connecting your server to MongoDB with your connection URI.

Now create a index.js file that would be your main file. Then require and invoke the connectDB function to establish a connection to your database.

Example index.js

// index.js
const express = require("express");
const connectDB = require("./db");

const app = express();

connectDB(); // Connect to database
app.use(express.json()); // Middleware to parse JSON requests

// Define a simple route
app.get("/", (req, res) => {
  res.send("Hello World!!");
});

const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Run the server with the command npm run start in your terminal. If everything is set up correctly, you should see the message MongoDB connected in the terminal.

Well, congratulations! You just started your node server.

Defining the data model

The next step is to create a model to define the structure of the data stored in MongoDB. For this tutorial, you’ll create a model for managing a collection of "tasks" in the to-do app.

Inside your project create a models folder to keep your models organized. Inside the models folder, create a Task.js file to define the schema.

Define the Task Schema

To define the model of each task, add the following code to Task.js:

const mongoose = require('mongoose');

// Define the Task schema
const TaskSchema = new mongoose.Schema({
  title: {
    type: String,
    required: [true, 'Title is required'],
  },
  description: {
    type: String,
  },
  completed: {
    type: Boolean,
    default: false,
  },
});

// Create and export the Task model
const Task = mongoose.model('Task', TaskSchema);

module.exports = Task;

Explaining the schema fields

To keep things simple, you’ll be using just a few fields in your schema, with each field having:

  • title: A required string field to store the task's title.

  • description: An optional string field to store additional details about the task.

  • completed: A boolean field indicating whether the task is done. By default, it’s set to false.

With this schema defined, you can now use the Task model to interact with the tasks collection in MongoDB.

Building the API Endpoints

The next step is to create API endpoints for handling CRUD operations in the todo app. These endpoints will allow you to interact with the tasks collection in MongoDB through requests.

Setting up routers

The first step is to set up routers, which creates the routes that define endpoints. Create a folder named routes and a file named task.js inside it. Add the following code to your

// routes/task.js
const express = require('express');
const Task = require('../models/Task'); // Import the Task schema

const router = express.Router(); // Initialize the router

// Endpoints goes here

// Export the router for use in other files
module.exports = router;

The next step is to make your requests with the router.

Creating a Task

Creating a task is done using a POST request, which is typically used to send data to the server to create a new resource.

// Creating a new task
router.post('/', async (req, res) => {
  try {
    const task = new Task(req.body);
    await task.save();
    res.status(201).json(task);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

Here’s a quick explanation of the code:

The router.post('/', ...) function that handles the POST request sent to /api/tasks You used / here because the router will be mounted under /api/tasks in the main app file.

The new Task(req.body) creates a new task instance using the data from the request’s body and task.save() saves the task to the tasks collection in your database.

If successful, the server responds with a status code 201 (Created) and the saved task data, but if there’s an error, a 500 status code is sent with an error message.

Fetching Tasks

The next endpoint you’ll implement allows the client to fetch a list of all tasks stored in the database. This is done using a GET request, which is typically used to fetch data from the server.

// Get all tasks
router.get("/", async (req, res) => {
  try {
    const tasks = await Task.find(); // Fetch all tasks from the database
    res.status(200).json({ tasks }); // Return the list of tasks
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

To retrieve tasks, the router.get('/', ...) function is used to handle GET requests sent to /api/tasks. This endpoint will fetch all tasks from the database using the Task.find() method.

Once all the tasks are retrieved, the server responds with a status code 200 and sends the tasks in JSON format. If an error occurs during this process, the server responds with a 500 status code and an error message to help diagnose the problem.

Updating a Task

The PUT request allows you to update an existing task in the database. You’ll handle this by using the task's ID⁣ to locate it, and then you’ll update the fields that are passed in the request.

// Update a task
router.put("/:id", async (req, res) => {
  try {
    const { title, description, completed } = req.body;
    const { id } = req.params; // Extract the task ID from the URL

    // Find the task by ID and update it
    const updatedTask = await Task.findByIdAndUpdate(
      id,
      { title, description, completed },
      { new: true } // Return the updated task
    );

    // If no task is found, return an error
    if (!updatedTask) {
      return res.status(404).json({
        message: "Task not found",
      });
    }
    // Respond with the updated task
    res.status(200).json({ data: updatedTask });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

Here, the router.put('/:id', ...) function handles PUT requests sent to /api/tasks/:id, where :id is a placeholder for the task’s unique identifier. This endpoint is responsible for updating an existing task with the specified ID.

The new task data is extracted from req.body, while req.params.id contains the ID of the task to be updated. Using Task.findByIdAndUpdate(id, updateData, options), the task is located and updated with the provided data. The { new: true } option ensures that the updated task is returned in the response.

If the task with the specified ID isn’t found, the server responds with a 404 status and a message indicating that the task doesn’t exist. For any other errors, a 500 status is sent along with the error message to help debug the issue.

Deleting a Task

The DELETE request allows you to delete a specific task from the database using its ID.

// Delete a task
router.delete('/:id', async (req, res) => {
  try {
    const { id } = req.params; // Extract the task ID from the URL
    // Find and delete the task by ID
    const deletedTask = await Task.findByIdAndDelete(id);
    // If no task is found, return an error message
    if (!deletedTask) {
      return res.status(404).json({
        message: 'Task not found',
      });
    }
    res.status(200).json({
      message: 'Task deleted successfully',
    });
  } catch (error) {
    res.status(500).json({
      error: error.message,
    });
  }
});

The router.delete('/:id', ...) function handles DELETE requests sent to /api/tasks/:id, where :id represents the unique identifier of the task to be deleted. This endpoint is designed to remove a specific task from the database.

The task ID is extracted from the URL using req.params.id. The Task.findByIdAndDelete(id) method is then used to find and delete the task with the specified ID. If the task doesn’t exist, the server responds with a 404 status and a message indicating that the task couldn’t be found.

After the task has been deleted, the server returns a 200 status along with a success message. In case of any errors during this process, the server catches the error and responds with a 500 status and the corresponding message.

Summary

In summary, your routes/tasks.js file should finally look like this:

// routes/task.js
const express = require("express");
const Task = require("../models/Task"); // Import the Task schema

const router = express.Router(); // Initialize the router

// Creating a new task
router.post("/", async (req, res) => {
  try {
    const task = new Task(req.body);
    await task.save();
    res.status(201).json(task);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// Get all tasks
router.get("/", async (req, res) => {
  try {
    const tasks = await Task.find(); // Fetch all tasks from the database
    res.status(200).json({ tasks }); // Return the list of tasks
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// Update a task
router.put("/:id", async (req, res) => {
  try {
    const { title, description, completed } = req.body;
    const { id } = req.params; // Extract the task ID from the URL

    // Find the task by ID and update it
    const updatedTask = await Task.findByIdAndUpdate(
      id,
      { title, description, completed },
      { new: true } // Return the updated task
    );

    // If no task is found, return an error
    if (!updatedTask) {
      return res.status(404).json({
        message: "Task not found",
      });
    }
    // Respond with the updated task
    res.status(200).json({ data: updatedTask });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// Delete a task
router.delete('/:id', async (req, res) => {
  try {
    const { id } = req.params; // Extract the task ID from the URL
    // Find and delete the task by ID
    const deletedTask = await Task.findByIdAndDelete(id);
    // If no task is found, return an error message
    if (!deletedTask) {
      return res.status(404).json({
        message: 'Task not found',
      });
    }
    res.status(200).json({
      message: 'Task deleted successfully',
    });
  } catch (error) {
    res.status(500).json({
      error: error.message,
    });
  }
});

// Export the router for use in other files
module.exports = router;

Don’t forget to call your router in your index.js file after creating the endpoints

// index.js
const tasksRouter = require('./routes/tasks');

// Using the tasks router
app.use('/api/tasks', tasksRouter);

Finally, your folder structure should look a lot like this:

Testing the Endpoints

You can use tools like Postman, Thunder Client, or any API testing tool to test the endpoints.

1. Creating a Task

  • HTTP Method: POST

  • URL: http://localhost:5000/api/tasks

  • Body: Add a JSON body with the task data:

      {
        "title": "Buy groceries",
        "description": "Milk, bread, and eggs",
        "completed": false
      }
    
  • Expected Result: The response should include the newly created task with a status code of 201.

2. Get All Tasks

  • HTTP Method: GET

  • URL: http://localhost:5000/api/tasks

  • Expected Result: The response should include a list of all tasks. If there are no tasks, you’ll see an empty array ([]).

3. Update a Task

  • HTTP Method: PUT

  • URL: http://localhost:5000/api/tasks/:id
    (Replace :id with the actual ID of the task you want to update.)

  • Body: Add the updated task data in the JSON format:

      {
        "title": "Updated Task Title",
        "description": "Updated task description",
        "completed": true
      }
    
  • Expected Result: The response should include the updated task with a status code of 200.

4. Delete a Task

  • HTTP Method: DELETE

  • URL: http://localhost:5000/api/tasks/:id
    (Replace :id with the actual ID of the task you want to delete.)

  • Expected Result: A confirmation message such as:

      {
        "message": "Task deleted successfully"
      }
    

These tests make sure that your endpoints are functioning as expected. If any test fails, check your code for errors, validate the data, and ensure your server and MongoDB are running correctly.

Finalizing the Project

To prepare your project to be scalable and maintainable, it’s best to structure it into distinct folders such as routes/⁣, models/ and config/⁣. Also make sure you store sensitive information like the database URI and API keys in a .env file and use the dotenv package to access them in your code.

For deployment, you can host your server on platforms like Heroku and Vercel. Both platforms offer free tiers, making it easy to deploy your REST API.

Conclusion

To sum it up:

  • You learned how to create a simple REST API using Express and MongoDB, implementing the fundamental CRUD operations (Create, Read, Update, Delete).

  • You also saw how to set up and structure your project to keep it clean and scalable.

  • You’ve also tested your API with Postman or Thunder Client.

With this, you can now start building your own REST APIs and even expand it further by adding more features, such as authentication or proper data validation when making requests.