How to Build a ToDo App Using MongoDB Aggregations (Full Stack Guide)

17/07/2025

How to Build a ToDo App Using MongoDB Aggregations (Full Stack Guide)

Learn how to build a ToDo app using MongoDB aggregations to enhance data queries. This guide covers backend logic, aggregation pipelines, and building a feature-rich UI with React and Node.js.

Beyond Basic Lists: Building a ToDo App with MongoDB Aggregations

A ToDo application might seem simple at first glance – just a list of tasks. However, as your task list grows, or if you want to gain insights into your productivity, basic CRUD (Create, Read, Update, Delete) operations might not be enough. This is where the power of MongoDB's Aggregation Framework comes into play, allowing you to process data records and return computed results.

This guide will demonstrate how to build a simple ToDo application using Node.js (Express) and MongoDB, with a special focus on leveraging MongoDB Aggregations to provide advanced analytics and summaries of your tasks.

What are MongoDB Aggregations and Why Use Them in a ToDo App?

The MongoDB Aggregation Framework is a powerful tool that allows you to perform complex data processing operations on your documents. It works by passing documents through a pipeline of stages, each stage transforming the documents as they pass through. Common aggregation operations include:

  • Filtering (`$match`): Selecting documents that match specified conditions.
  • Grouping (`$group`): Grouping documents by a specified key and performing aggregate functions (e.g., count, sum, average) on the grouped data.
  • Projecting (`$project`): Reshaping documents, including adding new fields, removing existing fields, or reshaping existing fields.
  • Sorting (`$sort`): Ordering documents.
  • Limiting (`$limit`) & Skipping (`$skip`): For pagination.

For a ToDo app, aggregations can provide valuable insights that simple `.find()` queries cannot:

  • Task Summaries: Quickly get a count of all tasks, completed tasks, and pending tasks.
  • Progress Tracking: Calculate the percentage of tasks completed.
  • Categorized Views: Group tasks by category, priority, or due date.

Project Setup: Node.js, Express, and MongoDB

We'll set up a basic Node.js/Express backend to handle our ToDo logic and connect to MongoDB.

1. Initialize Project and Install Dependencies

Create a new project directory and install the necessary packages:

mkdir todo-aggregation-app
cd todo-aggregation-app
npm init -y
npm install express mongoose cors dotenv

2. Environment Variables (`.env`)

Create a `.env` file in your project root and add your MongoDB connection string:

MONGO_URI=mongodb+srv://<username>:<password>@<cluster-url>/todoapp?retryWrites=true&w=majority
PORT=5000

3. ToDo Model (`models/Todo.js`)

Create `models/Todo.js`:

// models/Todo.js

const mongoose = require('mongoose');

const TodoSchema = new mongoose.Schema({
    task: {
        type: String,
        required: true,
        trim: true,
    },
    completed: {
        type: Boolean,
        default: false,
    },
    createdAt: {
        type: Date,
        default: Date.now,
    },
    category: { // New field for demonstration
        type: String,
        enum: ['Work', 'Personal', 'Shopping', 'Other'],
        default: 'Other',
    }
});

module.exports = mongoose.model('Todo', TodoSchema);

Backend Logic: Express.js with Aggregations

Now, let's create our `server.js` file to handle CRUD operations and implement several aggregation pipelines.

`server.js`

// server.js

const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const Todo = require('./models/Todo'); // Import Todo model
require('dotenv').config();

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

// Middleware
app.use(cors());
app.use(express.json()); // For parsing application/json

// MongoDB Connection
mongoose.connect(process.env.MONGO_URI)
    .then(() => console.log('MongoDB connected'))
    .catch(err => console.error('MongoDB connection error:', err));

// --- CRUD Routes for ToDo Items ---

// Get all ToDo items
app.get('/api/todos', async (req, res) => {
    try {
        const todos = await Todo.find({}).sort({ createdAt: -1 }); // Sort by newest first
        res.status(200).json(todos);
    } catch (error) {
        res.status(500).json({ message: 'Error fetching todos', error: error.message });
    }
});

// Create a new ToDo item
app.post('/api/todos', async (req, res) => {
    try {
        const newTodo = new Todo(req.body);
        await newTodo.save();
        res.status(201).json(newTodo);
    } catch (error) {
        res.status(400).json({ message: 'Error creating todo', error: error.message });
    }
});

// Update a ToDo item
app.put('/api/todos/:id', 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 updating todo', error: error.message });
    }
});

// Delete a ToDo item
app.delete('/api/todos/:id', 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 successfully' });
    } catch (error) {
        res.status(500).json({ message: 'Error deleting todo', error: error.message });
    }
});

// --- Aggregation Routes ---

// Get overall ToDo statistics (total, completed, pending counts)
app.get('/api/todos/stats', async (req, res) => {
    try {
        const stats = await Todo.aggregate([
            {
                $group: {
                    _id: null, // Group all documents together
                    totalTasks: { $sum: 1 }, // Count all documents
                    completedTasks: { $sum: { $cond: ['$completed', 1, 0] } }, // Sum 1 if completed is true, else 0
                    pendingTasks: { $sum: { $cond: ['$completed', 0, 1] } } // Sum 1 if completed is false, else 0
                }
            },
            {
                $project: {
                    _id: 0, // Exclude the _id field
                    totalTasks: 1,
                    completedTasks: 1,
                    pendingTasks: 1,
                    // Calculate completion percentage
                    completionPercentage: {
                        $cond: {
                            if: { $eq: ['$totalTasks', 0] }, // Avoid division by zero
                            then: 0,
                            else: { $multiply: [{ $divide: ['$completedTasks', '$totalTasks'] }, 100] }
                        }
                    }
                }
            }
        ]);

        // If no tasks exist, stats will be an empty array. Provide default values.
        const result = stats.length > 0 ? stats[0] : { totalTasks: 0, completedTasks: 0, pendingTasks: 0, completionPercentage: 0 };

        res.status(200).json(result);
    } catch (error) {
        console.error('Aggregation stats error:', error);
        res.status(500).json({ message: 'Error fetching todo stats', error: error.message });
    }
});

// Get ToDo items grouped by category
app.get('/api/todos/by-category', async (req, res) => {
    try {
        const categorizedTodos = await Todo.aggregate([
            {
                $group: {
                    _id: '$category', // Group by the 'category' field
                    tasks: {
                        $push: { // Push the original task details into an array
                            _id: '$_id',
                            task: '$task',
                            completed: '$completed',
                            createdAt: '$createdAt'
                        }
                    },
                    count: { $sum: 1 } // Count tasks in each category
                }
            },
            {
                $sort: { _id: 1 } // Sort categories alphabetically
            }
        ]);
        res.status(200).json(categorizedTodos);
    } catch (error) {
        console.error('Aggregation by category error:', error);
        res.status(500).json({ message: 'Error fetching categorized todos', error: error.message });
    }
});

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

Explanation of Backend Code:

  • CRUD Routes: Standard routes for creating, reading, updating, and deleting individual ToDo items.
  • `/api/todos/stats` (GET) - Overall Statistics:
    • `$group: { _id: null, ... }`: Groups all documents into a single group, allowing us to compute overall statistics.
    • `totalTasks: { $sum: 1 }`: Counts the total number of documents (tasks).
    • `completedTasks: { $sum: { $cond: ['$completed', 1, 0] } }`: Uses the `$cond` operator (if-then-else) to sum `1` if `completed` is true, effectively counting completed tasks.
    • `$project`: Reshapes the output, renames fields, removes `_id`, and calculates `completionPercentage` using `$divide` and `$multiply`. It also includes a `$cond` to handle division by zero if `totalTasks` is 0.
  • `/api/todos/by-category` (GET) - Grouped by Category:
    • `$group: { _id: '$category', ... }`: Groups documents based on the value of their `category` field.
    • `tasks: { $push: { ... } }`: For each group (category), it creates an array named `tasks` and pushes the details of each task belonging to that category into it.
    • `count: { $sum: 1 }`: Counts the number of tasks within each category.
    • `$sort: { _id: 1 }`: Sorts the resulting categories alphabetically by their `_id` (which is the category name).

Frontend Interaction (React - Conceptual)

While this guide focuses on the backend and MongoDB Aggregations, a React frontend would seamlessly consume these API endpoints. You would use `fetch` or Axios to make requests to `/api/todos`, `/api/todos/stats`, and `/api/todos/by-category` and then render the data accordingly.

For example, to display stats:

// client/src/components/TodoStats.js (Conceptual)

import React, { useEffect, useState } from 'react';

function TodoStats() {
  const [stats, setStats] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchStats = async () => {
      try {
        const response = await fetch('/api/todos/stats');
        const data = await response.json();
        if (response.ok) {
          setStats(data);
        } else {
          setError(data.message || 'Failed to fetch stats');
        }
      } catch (err) {
        setError(err.message || 'Network error');
      } finally {
        setLoading(false);
      }
    };
    fetchStats();
  }, []);

  if (loading) return <p style={{color: '#007bff', boxSizing: 'border-box'}}>Loading statistics...</p>;
  if (error) return <p style={{color: 'red', boxSizing: 'border-box'}}>Error: {error}</p>;
  if (!stats) return <p style={{color: '#555', boxSizing: 'border-box'}}>No stats available.</p>;

  return (
    <div style={{
      backgroundColor: '#f0f8ff',
      padding: '20px',
      borderRadius: '8px',
      marginTop: '30px',
      boxShadow: '0 2px 5px rgba(0,0,0,0.1)',
      textAlign: 'left',
      boxSizing: 'border-box'
    }}>
      <h3 style={{fontFamily: 'Merriweather, serif', color: '#008080', marginBottom: '15px', boxSizing: 'border-box'}}>ToDo Overview</h3>
      <p style={{marginBottom: '8px', boxSizing: 'border-box'}}><strong style={{boxSizing: 'border-box'}}>Total Tasks:</strong> {stats.totalTasks}</p>
      <p style={{marginBottom: '8px', boxSizing: 'border-box'}}><strong style={{boxSizing: 'border-box'}}>Completed Tasks:</strong> {stats.completedTasks}</p>
      <p style={{marginBottom: '8px', boxSizing: 'border-box'}}><strong style={{boxSizing: 'border-box'}}>Pending Tasks:</strong> {stats.pendingTasks}</p>
      <p style={{fontWeight: 'bold', color: '#008080', boxSizing: 'border-box'}}>Completion Rate: {stats.completionPercentage.toFixed(2)}%</p>
    </div>
  );
}

export default TodoStats;

You would then integrate this `TodoStats` component into your main `App.js` or a dashboard page.

Conclusion

While a basic ToDo app can be built with simple CRUD operations, leveraging MongoDB's Aggregation Framework unlocks a new level of data insight and functionality. By creating aggregation pipelines on your Node.js/Express backend, you can efficiently calculate statistics, group tasks by various criteria, and provide a richer user experience. This approach not only enhances the features of your ToDo application but also demonstrates the powerful capabilities of MongoDB for data analysis.