DEV Community

Cover image for Steps to Create A To-Do App With MERN stack
Chytra K R
Chytra K R

Posted on

Steps to Create A To-Do App With MERN stack

Tools You Will Need

Make sure Node and NPM are installed on your computer. You can download both at nodejs.org (NPM is included in your Node installation)

Tech Stack

Node.js
Express.js
MongoDB
React.js

Dependencies for Node.js

body-parser
mongoose
mongoose-auto-increment

Create your Node (Express) Backend

First create a folder for your project, called to-do-node (for example).
Then, open that folder in your code editor.
To create our Node project, run the following command in your terminal:

npm init -y

This will create a package.json file that will allow us to keep track of all our app scripts and manage any dependencies our Node app needs.

We'll use Express to create a simple web server for us that runs on port 3000.

So let's create an index file where our app starts to run with the name of index.js.

const express = require('express')
const app = express()
const bodyParser = require('body-parser');

//import router
const router = require('./app/index.js');

// body parser
app.use(bodyParser.urlencoded({ limit: '100mb', extended: true }))
app.use(bodyParser.json({ limit: '100mb', extended: true }))

app.use(function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE");
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization");

  if ('OPTIONS' === req.method) {
    //respond with 200
    res.send(200);
  }
  else {
    //move on
    next();
  }
});

app.get('/', (req, res) => {
  res.send("incorrect route");
})

//add routes
const base = '/api/v1/';
app.use(base, router);

app.listen(process.env.PORT || 3000, () => console.log('Running on port 3000!'));
Enter fullscreen mode Exit fullscreen mode

Then in our terminal, we will install dependencies which we need:

npm I express mongoose body-parser nodemon mongoose-auto-increment

Create a folder called app inside the project folder and add index.js file to defines routes.

const express = require('express');

// Routes Import
const toDo = require("./toDo/index.js");

const router = express.Router();

// Adding Routes
router.use('/to-do', toDo);

module.exports = router;

Enter fullscreen mode Exit fullscreen mode

Create folder name called config and add db.js file to that.

db.js file will have the following configurations:

const mongoose = require('mongoose');
mongoose.connect('mongodb+srv://<mongodb_username>:<cluster_password>@cluster0.nxcni.mongodb.net/toDo?retryWrites=true&w=majority', { useNewUrlParser: true });
module.exports = mongoose.connection;
Enter fullscreen mode Exit fullscreen mode

Let's write APIs now.

Create a folder called toDo and model.js, index.js, and router.js files in that.

index.js

module.exports = require("./router");
Enter fullscreen mode Exit fullscreen mode

model.js

const mongoose = require('mongoose'); 
const autoIncrement = require('mongoose-auto-increment');
const db = require('../config/db.js');

autoIncrement.initialize(db);

const schema = new config.mongoose.Schema({
  toDos: [
    {
      toDo: String,
      tag: String,
      tagColor: String,
      done: Boolean
    }
  ],
  createdAt: {
    type: Date,
    default: Date.now
  },
  updatedAt: {
    type: Date,
    default: Date.now
  },
  status: {
    type: Boolean,
    default: true
  }
}, {
  strict: true
});

var toDo = mongoose.model('toDos', schema);
module.exports = toDo;
Enter fullscreen mode Exit fullscreen mode

router.js

const config = require('../config/routes.js');
const router = config.express.Router();
const collection = require('./model.js');

// @route GET api/v1/to-do/list
// @desc get users list with pagination
// @access Public
router.get('/list', function (req, res) {
    if(!req.query.id) {
          res.status(200).send({data: []});
        return false;
    }
    getToDosList(req.query.tag ? req.query.tag : '', req.query.id).then(resp => {
          res.status(200).send(resp[0]);
    }, err => {
          res.status(500).send({message: "Something went wrong, please try after sometime"});
    })
});

// @route CREATE api/v1/to-do/add
// @desc add to-do
// @access Public
router.post('/add', function(req, res) {
    if(!req.query.id) {
        collection.create({toDos: [{toDo: req.body.text, done: false, tag: req.body.tag, tagColor: req.body.tagColor}]}, function (err, toDo) {
            if (!err) {
                return res.status(200).json({error: false, data: toDo, message: 'success'})
            } else {
                  return res.status(500).send({error: true, message: 'Error adding to-do'})
            }
        });
    } else {
            let updateData = {
            $push: {
                "toDos": {toDo: req.body.text, done: false, tag: req.body.tag, tagColor: req.body.tagColor}
            }
        };
            updateToDo({_id: req.query.id}, updateData).then(toDo => {
              return res.status(200).json({error: false, data: toDo, message: 'success'})
            }, err => {
            return res.status(500).send({error: true, message: 'Error adding to-do'})
            })
    }
});

// @route UPDATE api/v1/to-do/done
// @desc update toDo status
// @access Public
router.put('/done/:userId/:toDoId', function(req, res) {
    let updateData = {
        $set: {
            "toDos.$.done": req.body.done
        }
    };
    updateToDo({_id: req.params.userId, "toDos._id": req.params.toDoId}, updateData).then((toDo) => {
        return res.status(200).json({error: false, message: 'Updated successfully'})
    }, err => {
        return res.status(500).send({error: true, message: err})
    })
});

// @route UPDATE api/v1/to-do/delete
// @desc delete toDo
// @access Public
router.put('/delete/:userId/:toDoId', function(req, res) {
    let updateData = { "$pull": { "toDos": { "_id": req.params.toDoId } } }
    updateToDo({_id: req.params.userId, "toDos._id": req.params.toDoId}, updateData).then((toDo) => {
        return res.status(200).json({error: false, message: 'Updated successfully'})
    }, err => {
        return res.status(500).send({error: true, message: err})
    })
});

// function to get to-dos list with tag filter
function getToDosList(tag, id) {
    return new Promise(function(resolve, reject) {
        let agg = [
            {
                "$unwind": "$toDos"
            }, {
                "$match": {
                    $or: [{"_id": id}, {"toDos.tag": {$regex: `${tag}.*`, $options: "i" }}]
                }
            }, {
                "$group": {
                    _id: null,
                    data: {$push: "$toDos"}
                }
            }
        ]
        collection.aggregate(agg, function(err, response) {
          if(err) return reject({message: "Something went wrong"})
          if(!response) return reject({message: "Error while getting remitters data"})
          return resolve(response)
        })
    })
}

//function to update to-do
function updateToDo(query, updateData) {
    return new Promise(function(resolve, reject) {
        collection.findOneAndUpdate(query, updateData, {new: true},
            function (err, resp) {
                    if (err) return reject({error: 1, message: "There was a problem while updating data"});
                    return resolve(resp);
            }
        );
    })
}

function getToDos(query) {
    return new Promise(function(resolve, reject) {
        collection.find(query,
            function (err, resp) {
                    if (err) return reject({error: 1, message: "There was a problem while updating data"});
                    return resolve(resp);
            }
        );
    })
}


module.exports = router

Enter fullscreen mode Exit fullscreen mode

Finally, we can run our app by running nodemon index.js in our terminal and we should see that app is running on port 3000.

Dependencies for React.js

bootstrap
react-bootstrap
react-icons

Create your React Frontend

After creating our backend, let's move to the frontend.

Open another terminal tab and use create-react-app to create a new React project with the name to-do-react (for example):

npx create-react-app to-do-react

After that, we will have a React app with all of its dependencies installed.

Now, go to the folder

cd to-do-react

Change directory to src and run the below commands:

cd src

rm *

Now create index.js file by running below command:

touch index.js

This file will render our app to an HTML file which is in the public folder. Also, create a folder name components with the file name app.js.

mkdir components && cd components && touch app.js

app.js will contain our To-Do app.

Edit index.js file in src:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/app';
import 'bootstrap/dist/css/bootstrap.min.css';

ReactDOM.render(<App/>, document.getElementById('root'));
Enter fullscreen mode Exit fullscreen mode

Create a folder with the name of api and add the file with name of to-do.js and write API calls in that file as below:

import axios from 'axios';

let base = 'http://localhost:3000/api/v1/';

export default function api(url, method='GET', data={}) {
    return new Promise(function(resolve, reject) {
        const requestOptions = {
            url: base + url,
            method: method,
            headers: {
                'Content-Type': 'application/json'
            },
            data
        };
        axios(requestOptions)
        .then(function (response) {
            resolve(response.data);
        })
        .catch(function (error) {
            reject(error);
        });
    });
}

export function AddToDoAPI(data) {
    return new Promise(function(resolve, reject) {
        api(`to-do/add?id=${localStorage.userId ? localStorage.userId : ''}`, 'POST', data)
        .then((resp) => {
            return resolve(resp);
        }, (error) => {
            return reject(error.response.data.message);
        })
    })
}

export function GetToDoListAPI(tag='') {
    return new Promise(function(resolve, reject) {
        api(`to-do/list?id=${localStorage.userId ? localStorage.userId : ''}&tag=${tag}`)
        .then((resp) => {
            return resolve(resp);
        }, (error) => {
            console.log(error)
            debugger
            return reject(error.response.data.message);
        })
    })
}

export function UpdateToDoAPI(data, toDoId) {
    return new Promise(function(resolve, reject) {
        api(`to-do/done/${localStorage.userId}/${toDoId}`, 'PUT', data)
        .then((resp) => {
            return resolve(resp);
        }, (error) => {
            return reject(error.response.data.message);
        })
    })
}

export function DeleteToDoAPI(toDoId) {
    return new Promise(function(resolve, reject) {
        api(`to-do/delete/${localStorage.userId}/${toDoId}`, 'PUT', {})
        .then((resp) => {
            return resolve(resp);
        }, (error) => {
            return reject(error.response.data.message);
        })
    })
}
Enter fullscreen mode Exit fullscreen mode

Edit app.js in components:

import React, {Component} from 'react';

// Bootstrap for react
import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import Button from 'react-bootstrap/Button';
import InputGroup from 'react-bootstrap/InputGroup';
import FormControl from 'react-bootstrap/FormControl';
import ListGroup from 'react-bootstrap/ListGroup';
import Form from 'react-bootstrap/Form'
import Dropdown from 'react-bootstrap/Dropdown'
import DropdownButton from 'react-bootstrap/DropdownButton'
import {AddToDoAPI, GetToDoListAPI, UpdateToDoAPI, DeleteToDoAPI} from '../api/to-do'
import { BsStop, BsX } from 'react-icons/bs';
import {Badge} from "react-bootstrap";

class AppComponent extends Component {
    constructor(props) {
        super(props);

        // Setting up state
        this.state = {
            userInput : "",
            list:[],
            selectedTag: "Other",
            selectedTagColor: "grey",
            tags: [
                {tagName: 'Other', color: 'grey'},
                {tagName: 'Work', color: 'red'},
                {tagName: 'Personal', color: 'green'}
            ]
        }
    }
    componentDidMount() {
        this.getItems()
    }

    // Set a user input value
    updateInput(value){
        this.setState({
            userInput: value,
        });
    }

    // Set a selected tag value
    updateTag(value){
        this.setState({
            selectedTag: value.split(" ")[0],
            selectedTagColor: value.split(" ")[1]
        });
    }

    // Add item if user input in not empty
    addItem(event){
        if(event.code === 'Enter') {
            AddToDoAPI({text: this.state.userInput, tag: this.state.selectedTag, tagColor: this.state.selectedTagColor}).then(resp => {
                if(!localStorage.userId) {
                    localStorage.setItem('userId', resp.data._id);
                }
                this.getItems()
            })
        }
    }

    //Get to-do list
    getItems(tag='') {
        GetToDoListAPI(tag).then(resp => {
            // Update list
            const list = [...resp ? resp.data : []];
            // reset state
            this.setState({
                list,
                userInput: ""
            });
        })
    }

    UpdateToDo(val, id) {
        UpdateToDoAPI({done: val}, id).then(resp => {
            this.getItems()
        })
    }

    // Function to delete item from list use id to delete
    deleteItem(id) {
        DeleteToDoAPI(id).then(resp => {
            this.getItems()
        })
    }

    render(){
        return(
        <Container>

            <Row style={{
                display: "flex",
                justifyContent: "center",
                alignItems: "center",
                fontSize: '3rem',
                fontWeight: 'bolder',
                fontFamily: 'DejaVu Sans Mono, monospace',
                paddingTop: 2
                }}
                >TODO LIST
            </Row>
            <hr style={{marginTop: 0}}/>
            <Row>
                <Col md={{ span: 5, offset: 4 }}>
                    <InputGroup className="mb-3">
                        <DropdownButton
                            variant="outline-secondary"
                            id="input-group-dropdown-2"
                            title={this.state.selectedTag}
                            align="end"
                            size="lg"
                            style={{backgroundColor: 'white'}}
                            onSelect = {e => this.updateTag(e)}
                            >
                                {this.state.tags.map(tag => (
                                    <span style={{display: 'flex'}}><BsStop style={{fontSize: 30, marginTop: 1, color: tag.color}}/><Dropdown.Item key={tag.tagName} eventKey={tag.tagName + ' ' + tag.color}>{tag.tagName}</Dropdown.Item></span>
                                ))}
                        </DropdownButton>
                        <FormControl
                            placeholder="add item . . . "
                            size="lg"
                            value = {this.state.userInput}
                            onChange = {item => this.updateInput(item.target.value)}
                            onKeyPress = {e => this.addItem(e)}
                            aria-label="add something"
                            aria-describedby="basic-addon2"
                        />
                    </InputGroup>
                </Col>
            </Row>
            {this.state.list.length ? 
                <Row>
                    <Col md={{ span: 5, offset: 4 }} style={{paddingBottom: 18}}>
                        <Button variant="primary" style={{paddingTop: 0, paddingBottom: 0}} onClick={e => this.getItems('')} size="sm">All</Button>{' '}
                        <Button variant="secondary" style={{paddingTop: 0, paddingBottom: 0}} onClick={e => this.getItems('Other')} size="sm">Other</Button>{' '}
                        <Button variant="danger" style={{paddingTop: 0, paddingBottom: 0}} onClick={e => this.getItems('Work')} size="sm">Work</Button>{' '}
                        <Button variant="success" style={{paddingTop: 0, paddingBottom: 0}} onClick={e => this.getItems('Personal')} size="sm">Personal</Button>{' '}
                    </Col>
                </Row> : null
            }
            <Row>
                <Col md={{ span: 5, offset: 4 }}>
                    <ListGroup>
                    {/* map over and print items */}
                    {this.state.list.map(item => {return(

                        <ListGroup.Item variant="white" action
                             key={item._id}>
                            <Form.Group id="formGridCheckbox" style={{display: 'flex'}}>
                            <Form.Check type="checkbox" style={{width: 10}} className="my-1 mr-sm-2" onChange={e => this.UpdateToDo(!item.done, item._id)} checked={item.done}/>
                            {item.done ? <span style={{textDecoration: 'line-through', marginTop: 5, width: 380}}>{item.toDo}</span> : <span style={{marginTop: 5, width: 380}}>{item.toDo}</span>}
                            <BsStop style={{fontSize: 25, marginTop: 1, color: item.tagColor, float: 'right', width: 30}}/>
                            <BsX
                                onClick = { () => this.deleteItem(item._id) }
                                style={{float: 'right', fontSize: 25, marginLeft: 'auto', width: 30}}
                            />
                            </Form.Group>
                        </ListGroup.Item>
                    )})}
                    </ListGroup>
                </Col>
            </Row>
        </Container>
        );
    }
}

export default AppComponent;
Enter fullscreen mode Exit fullscreen mode

Start the server by typing the following command in the terminal:

npm start

Output: Open http://localhost:3000 in browser:

To-Do app

Also, Apply filters and see the personalized to-dos:
To-Do app

Top comments (0)