Steps to Create A To-Do App With MERN stack
Published: 2021-06-18 10:54:10

## 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](https://nodejs.org/en/)
[Express.js](https://expressjs.com/en/starter/installing.html)
 [MongoDB](https://www.mongodb.com/cloud)
[React.js](https://reactjs.org/docs/getting-started.html)

## Dependencies for Node.js
[body-parser](https://www.npmjs.com/package/body-parser)
[mongoose](https://www.npmjs.com/package/mongoose)
[mongoose-auto-increment](https://www.npmjs.com/package/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!'));
``` 

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;

``` 

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;
``` 

## 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");
``` 
### 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;
```

### 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

``` 
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](https://www.npmjs.com/package/bootstrap)
[react-bootstrap](https://www.npmjs.com/package/react-bootstrap)
[react-icons](https://www.npmjs.com/package/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'));
``` 

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);
        })
    })
}
``` 

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;
``` 

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

`npm start`

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

![To-Do app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m41f3clondcm2830obv1.png)

Also, Apply filters and see the personalized to-dos:
![To-Do app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/alt66asdpvbp1l4pq5mw.png)