Building a Todo API with Node.js, Express, and MongoDB

In this article, we’ll walk through the process of creating a robust Todo API using Node.js, Express, and MongoDB. This API will allow users to create, read, update, and delete todo items. We’ll cover the setup, file structure, and implementation of each component.

Part Two Flutter Integration: https://raheemdev.com/building-the-flutter-frontend-for-a-todo-app-with-node-js-rest-api/

Project Structure

Before we dive into the code, let’s look at our project structure:

/todo_api
│
├── /config
│   └── db.js          # MongoDB connection
├── /controllers
│   └── todoController.js  # Controller for handling CRUD logic
├── /models
│   └── todoModel.js   # Todo Mongoose model
├── /routes
│   └── todoRoutes.js  # Route definitions
├── .env               # Environment variables
├── app.js             # Express setup
└── package.json       # Dependencies

Install the necessary dependencies by running:

npm install express mongoose dotenv

Step 1: Setting Up MongoDB Connection

First, let’s set up our MongoDB connection. Create a file named db.js in the config folder:

const mongoose = require('mongoose');

const connectDB = async () => {
    try {
        const conn = await mongoose.connect(process.env.MONGO_URI, {
            useNewUrlParser: true,
            useUnifiedTopology: true,
        });
        console.log(`MongoDB connected: ${conn.connection.host}`);
    } catch (error) {
        console.error(`Error: ${error.message}`);
        process.exit(1); // Exit process with failure
    }
};

module.exports = connectDB;

This function will handle the connection to our MongoDB database using the provided URI.

Step 2: Creating the Todo Model

Next, let’s define our Todo model. Create a file named todoModel.js in the models folder:

const mongoose = require('mongoose');

const todoSchema = new mongoose.Schema({
    title: {
        type: String,
        required: true,
    },
    body: {
        type: String,
        required: true
    },
    completed: {
        type: Boolean,
        default: false,
    },
}, { timestamps: true });

const Todo = mongoose.model('Todo', todoSchema); // todos -> collection

module.exports = Todo;

This model defines the structure of our todo items, with a title, body, and a completed status. The body field is now required.

Step 3: Implementing CRUD Operations

Now, let’s create our controller to handle CRUD operations. Create a file named todoController.js in the controllers folder:

const Todo = require('../models/todoModel');

// Get all todos
const getTodos = async (req, res) => {
    try {
        const todos = await Todo.find();
        res.status(200).json(todos);
    } catch (error) {
        res.status(500).json({ message: error.message });
    }
};

// Create new todo
const createTodo = async (req, res) => {
    try {
        const todo = new Todo({
            title: req.body.title,
            body: req.body.body,
            completed: req.body.completed || false,
        });
        const savedTodo = await todo.save();
        res.status(201).json(savedTodo);
    } catch (error) {
        res.status(400).json({ message: error.message });
    }
};

// Update todo
const updateTodo = async (req, res) => {
    try {
        const updatedTodo = await Todo.findByIdAndUpdate(req.params.id, req.body, {
            new: true,
            runValidators: true,
        });
        if (!updatedTodo) return res.status(404).json({ message: "Todo not found" });
        res.status(200).json(updatedTodo);
    } catch (error) {
        res.status(400).json({ message: error.message });
    }
};

// Delete todo
const deleteTodo = async (req, res) => {
    try {
        const deletedTodo = await Todo.findByIdAndDelete(req.params.id);
        if (!deletedTodo) return res.status(404).json({ message: "Todo not found" });
        res.status(200).json({ message: "Todo deleted" });
    } catch (error) {
        res.status(500).json({ message: error.message });
    }
};

module.exports = {
    getTodos,
    createTodo,
    updateTodo,
    deleteTodo,
};

These functions handle getting all todos, creating a new todo, updating an existing todo, and deleting a todo. Note that the createTodo function now includes the body field when creating a new todo.

Step 4: Setting Up Routes

Let’s set up our routes to connect our API endpoints to the controller functions. Create a file named todoRoutes.js in the routes folder:

const express = require('express');
const { getTodos, createTodo, updateTodo, deleteTodo } = require('../controllers/todoController');

const router = express.Router();

router.get('/', getTodos);
router.post('/', createTodo);
router.put('/:id', updateTodo);
router.delete('/:id', deleteTodo);

module.exports = router;

This sets up our routes for each CRUD operation.

Step 5: Main Application Setup

Finally, let’s set up our main application file. Create a file named app.js in the root directory:

const express = require('express');
const dotenv = require('dotenv');
const connectDB = require('./config/db');
const todoRoutes = require('./routes/todoRoutes');

// Load environment variables
dotenv.config();

// Connect to MongoDB
connectDB();

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

// Todo Routes
app.use('/api/todos', todoRoutes);

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

This file brings everything together, setting up our Express application, connecting to the database, and implementing our routes.

Environment Variables

Don’t forget to set up your .env file in the root directory:

MONGO_URI=mongodb+srv://<username>:<password>@cluster.mongodb.net/todoDB
PORT=5000

Replace <username> and <password> with your actual MongoDB credentials.

Conclusion

With these components in place, you now have a fully functional Todo API built with Node.js, Express, and MongoDB. This API allows for creating, reading, updating, and deleting todo items, including a title and body for each todo. You can further expand on this foundation by adding user authentication, more complex querying, or additional features as needed for your specific use case.

Leave a Comment