Skip to content

Todos Example

Introduction

This example demonstrates how to create fullstack application with MERN stack. Application will handle a basic todo messages with text and date. User can add and delete todos.

intro

Complete source codes: todo-example. Remember fork it first, if you are going to try it!

Application architecture

The image below shows the architecture of the application.

  • Application UI uses HTML/CSS and JavaScript with React framework.
  • Information is passed between the user interface and the server in JSON format.
  • Node.js and Express technologies are used on the server side.
  • Todos are stored in the MongoDB database in the cloud service.

architecture

Backend

Project and packages

Create a new todo\server folder for a new Node.js project and install a below packages. You are using Express, Mongoose, dotenv to store environment variables and nodemon to restarting the node application when file changes in the directory are detected.

1
2
3
4
5
npm init -y
npm install express
npm install mongoose
npm install dotenv
npm install --save-dev nodemon

Modify your package.json to include nodemon starting script:

package.json
1
2
3
4
5
6
//...
"scripts": {
    "dev": "nodemon index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
//...

MongoDB in Atlas Cloud

You need to have an account to https://www.mongodb.com/. Create one free cluster, enable your IP, add database user/password. Create one database to use. Look more information from MongoDB materials.

Environment variables

Create .env file to project root and add a following variables. You need to use your own Cloud MongoDB URI. Go to https://www.mongodb.com/ and login with your account. Go to your cluster/database and click connect to get your connection string. Select Connect your application and your connection string. Modify below to use your own connectin string.

.env
1
2
PORT=3000
MONGODB_URI="mongodb+srv://pasi:<password>@democluster.brlob.mongodb.net/?retryWrites=true&w=majority"

Remember create .gitignore file and add .env and node_modules into it, so those will not be published to Git clouds.

.gitignore
1
2
.env
node_modules

Project starting code

Create index.js file to create an Express application. Add one route/endpoint to your app.

index.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
require('dotenv').config()
const express = require('express') 
const app = express()
const PORT = process.env.PORT

// json parser middleware
// parse incoming requests with JSON, assign it to requests body
app.use(express.json())

app.get('/api/test', (req, res) => {
    res.send('test')
})

app.listen(PORT, () => {
  console.log(`Todos app is listening on port ${PORT}`)
})

Start your server with nodemon:

1
npm run dev

Open your browser to http://localhost:3000/api/test to see that your api/test endpoint is working (and your Node.js/Express app is running correctly!).

Connection to database

Create a new db folder and a new mongodb.js file inside it. As for the mongodb.js, the database connection is separated and later imported in to index.js. The parameters are used for supressing deprecation warnings when initiating the connection.

db/mongodb.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const mongoose = require('mongoose')

// fix Mongoose strictQuery deprecation Warning
mongoose.set('strictQuery', false) 

const connectMongoDB = async (url) => {
  return mongoose.connect(url, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })
}

module.exports = connectMongoDB

And require it in index.js and create a connection to MongoDB.

index.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
require('dotenv').config()
const express = require('express') 
const app = express()
const PORT = process.env.PORT
const connectMongoDB = require('./db/mongodb')

app.use(express.json())

app.get('/api/test', (req, res) => {
    res.send('test')
})

// connect mongodb and start server
const start = async () => {
  console.log('Connecting to MongoDB')
  try {
    await connectMongoDB(process.env.MONGODB_URI)
    console.log('Connected to MongoDB')
    app.listen(
      process.env.PORT, 
      () => console.log(`Todos app is listening on port ${PORT}`)
    )
  } catch (error) {
    console.log(error)
  }
}

// start connection to mongodb and start server
start()

Note

1
We will first check if the connection to MongoDB can be established correctly. If this happens we will start the actual server.

Save your files and nodemon should start your server again. You should see a following information in your terminal if connection to MongoDB is working correctly.

1
2
3
4
5
[nodemon] restarting due to changes...
[nodemon] starting `node index.js`
Connecting to MongoDB
Connected to MongoDB
Todos app is listening on port 3000

Todos model

Create a new models folder and a new Todo.js file into it.

User.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const mongoose = require('mongoose')

const todoSchema = new mongoose.Schema({
  text: {
    type: String,
    minlength: 5,
    required: true
  },
  date: Date
})

const Todo = mongoose.model('Todo', todoSchema)

module.exports = Todo

Use created Todo model in your index.js.

index.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// ...
app.use(express.json())

// use Todo model 
const Todo = require('./models/todo')

app.get('/api/test', (req, res) => {
    res.send('test')
})
// ...

Routes

create a new todo

Create a new endpoint (for example below your api/test endpoint) which will be used to communicate with MongoDB. We are getting data from the client (later) in the body of the POST request and pass it on to the models with create() function, which is just a thin wrapper for the save() function.

index.js
1
2
3
4
5
6
7
8
9
app.post('/api/todos', async (req, res) => {
  try {
    const todo = await Todo.create(req.body)
    res.status(201).json({ success: true, data: todo })
  } catch (error) {
    console.log(error)
    res.status(400).json({ success: false, msg: 'Error with creating a todo!' })
  }
})

Use Postman and create a few new Todo documents to your MongoDB database. Click image to see it better!

!Image 01

Remember check that data is correctly written to your MongoDB database.

!Image 02

get todos

Create an endpoint to get all todos from MongoDB database.

index.js
1
2
3
4
5
6
7
8
app.get('/api/todos', async (req, res) => {
  try {
    const todos = await Todo.find({})
    res.status(200).json({ success:true, data:todos })
  } catch (error) {
    res.status(500).json({ success:false, msg: error })
  }
})

Use Postman to get all todo documents from MongoDB database. Click image to see it better!

!Image 03

get todo

Create an endpoint which will return one todo by a todo id from MongoDB database.

index.js
1
2
3
4
5
6
app.get('/api/todos/:id', async (req, res) => {
  const { id } = req.params
  const todo = await Todo.findById(id)
  if (todo) res.status(200).json({ success:true, data:todo })
  else res.status(500).json({ success:false, error:"Todo not found!" })
})

Use Postman to get one todo document from MongoDB database. Use one of your todo's id in request. Click image to see it better!

!Image 04

delete todo Create an endpoint which will be used to delete todo from MongoDB database.

index.js
1
2
3
4
5
6
app.delete('/api/todos/:id', async (req, res) => {
  const { id } = req.params
  const todo = await Todo.findByIdAndDelete(id)
  if (todo) res.status(200).json({ success:true, data:todo })
  else res.status(500).json({ success:false, error:"Todo not found!" })
})

Use Postman to delete one todo document from MongoDB database. Use one of your todo's id in request. Click image to see it better!

!Image 05

Now deleted todo document is sent back to caller application (Postman). Remember check that todo document is deleted from MongoDB database.

!Image 06

Now backend programming should be ready and you can move on to client side coding.

Frontend

Introduction

The purpose of this exercise is to learn the basics of the React programming. You will create a small Todo app with create-react-app.

You will use this React app with your already made Node.js application which is using Express and Cloud based MongoDB database.

intro

Create a new project

Your project structure will be as shown in below. You have separate folders for your client and backend side codes.

1
2
3
4
5
todo
├── server
   └── node application
├── client
    └── react application

Go to your todo folder and create a new React project called client. This code will create a client folder and React based application inside it. To get started go to your project client folder and start running development server.

1
2
3
4
cd todo
npx create-react-app client
cd client
npm start

Node server side app is running at port 3000

You might get a warning that something is already running on port 3000, which is default port for React app too. Just allow React to use another port instead. Your Node server side application (if it is running - should be ) is using port 3000.

1
2
3
4
5
? Something is already running on port 3000. Probably:
/usr/local/bin/node index.js (pid 4869)
in /Users/pasi/JamkDataModellingAndBackendDevelopment/FullStack/TodoExample/server

Would you like to run the app on another port instead? › (Y/n)

Your browser should show a React based application running with a spinning blue React logo.

Modify App.js

Open App.js in your Visual Studio Code editor and edit it to show “Hello React!” text. Save and see changes in your browser.

App.js
1
2
3
4
5
6
7
function App() {
  return (
    <div>
      <p>Hello React!</p>
    </div>
  );
}

You should see a below text in your browser.

Image 07

Add a new Banner component to the app

React uses HTML elements which can be used in applications. Of course, you can create your own components/functions too.

Create a Banner function, which will be used at the top of the ToDo app. Add a following code before App function. So, you can use multiple functions in the same file. Of course, you should separate all the functions/components to different files, if your project get’s bigger. Now our project is quite a small one.

App.js
1
2
3
4
5
function Banner() {
  return (
    <h1>Todo Example with React</h1>
  )
}

And remember use your Banner in App.

App.js
1
2
3
4
5
6
7
function App() {
  return (
    <div>
      <Banner/>
    </div>
  );
}

Remove all the default styles from the App.css file and use following styles. They will be used in this app.

App.css
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
body {
  font-family: 'Oswald', sans-serif;
}

h1 {
  background-color: cadetblue;
  text-align: center;
  color: white;
  padding: 5px 0;
  border-radius: 0 0 2px 2px;
}

ul {
  list-style-type: square;
  margin: 20px 0 20px 0;
}

input[type=text], input[type=password] {
  border: 1px solid #ccc;
  padding: 5px;
  width: 300px;
  font-size: 15px;
}

input[type=submit] {
  border: 1px solid #ccc;
  margin-left: 15px;
  padding: 5px 15px;
  font-size: 16px;
}

#root {
  width: 400px;
  background-color:aliceblue;
  border-radius: 3px;
  padding: 0 15px 15px;
  margin: 10px auto;
  border: 1px solid #ddd;
}

span {
  cursor: pointer;
  color: red;
  font-weight: bold;
}

input[type=button] {
  border: 1px solid #ccc;
  padding: 5px 15px;
  font-size: 16px;
  margin-top: 10px;
  margin-left: 10px;
}

.Error {
  color:red;
  font-size:10px;
}

Save you files and check your app in browser. Now a Banner component should be visible at the top of the application.

Image 08

Create an UI to show todos

A new todo will be asked with a HTML input element and all the todo items will be displayed within div element. Create a new ToDoFormAndList function with a below code. Now ul list will show some hardcoded values.

App.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function ToDoFormAndList() {
  return (
    <div>
      <form>
        <input type='text' placeholder="Write a new todo here"/>
        <input type='button' value='Add'/>
      </form>
      <ul>
        <li>First item : 2023-01-22</li>
        <li>Second item : 2023-01-23</li>
      </ul>   
    </div>
  )  
}

And remember use it in your App rendering.

App.js
1
2
3
4
5
6
7
8
function App() {
  return (
    <div>
      <Banner/>
      <ToDoFormAndList/>
    </div>
  );
}

Save you files and check your app in browser. Now a ToDoFormAndList component should be visible at the below of the Banner function. There are a few styles defined to input elements in the styles. Look them from your App.css. Now only a few hardcoded strings will be displayed in ul list.

Image 09

Load and show Todo items

First stop your server by clicking CTRL-C and install axios which can be used to communication between client and server side.

1
npm i axios

And use axios in your App.js:

1
import axios from 'axios';

Start your server again with below command:

1
npm start

ToDoFormAndList function should hold all the todo items in an array and a new item text. All these data should be saved in states, so you need to initialize these in your function. React Hooks will be used in this exercise.

Note

You will need to import React Hooks in your application. Modify you React import as shown below

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

Declare a new state variable to hold all todos inside ToDoFormAndList function. It will be empty by default and it can be changed with setTodos React Hooks function call.

App.js - ToDoFormAndList()
1
const [todos, setTodos] = useState([]); 

Next use React Hooks useEffect to load all todos from MongoDB database.

App.js - ToDoFormAndList()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
async function fetchData() {
  try {
    let response = await axios.get('http://localhost:3000/api/todos');
    // response.data is all data sent from server side
    console.log(response.data);
    // response.data.data is returned todos
    setTodos(response.data.data);
  } catch (error) {
    console.log(error);
  }
}

useEffect(() => {
  fetchData();
}, []) // load once, use => []

Save and run your code. You should get a following CORS error (open your browser inspector). That is normal behavior because we are running client and server in different resources in web. Read more information from here - CORS.

Image 10

We need to use cors middleware in Node/Express application to allow client request from different web origins.

First stop (cntr-c) your backend Node application and install cors module and restart your backend application:

1
2
npm i cors
npm run dev

Modify your express application index.js to use cors middleware.

index.js
1
2
3
4
5
const express = require('express')
const cors = require('cors')
const app = express()

app.use(cors())

Refresh your React client application in browser and you should see loaded todos in your browser inspector (if you have some todos in your MongoDB database).

Image 11

Next we will go back to the client side code and we will generate a list elements which will be used to show todos in UI.

App.js - ToDoFormAndList()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// create li-elements from todos data
const todoItems = todos.map((todo,index) =>
  <li key={index}>{todo.text} : {todo.date}</li>
);

// render 
return (
<div>
  <form>
    <input type='text' 
      value={newTodoText} 
      onChange={event => setNewTodoText(event.target.value)} 
      placeholder="Write a new todo here" />
    <input type='button' value='Add' onClick={addTodo}/>
  </form>
  <ul>
    { todoItems }
  </ul>   
</div>
)  

Save and test your code. You should see todo items now below a new todo input field.

Image 11

Now times are shown as they are stored in MongoDB database. We can use moment package to show those more nicely.

Stop your React app and install moment package. Start your React app again.

1
2
npm i moment
npm start

Import moment in your React app and modify your todos date display:

App.js
1
import moment from 'moment';
App.js - ToDoFormAndList()
1
2
3
4
// create li-elements from todos data
const todoItems = todos.map((todo,index) =>
    <li key={index}>{todo.text} : { moment(todo.date).format('DD.MM.YYYY')}</li>
);

Save and test your code. Now dates are showm much nicer.

Image 12

Add a new todo

Declare a new state variable to hold a new todo text inside ToDoFormAndList function. It will be empty as a default and it can be changed with setNewTodoText function call.

App.js - ToDoFormAndList()
1
const [newTodoText, setNewTodoText] = useState("");

And use newTodoText value inside a input element. Add onChange event to call React Hooks setNewTodoText function to change newTodoText text. Note how React Hooks can change variable state.

App.js - ToDoFormAndList()
1
2
3
4
<input type='text' 
       value={newTodoText} 
       onChange={event => setNewTodoText(event.target.value)} 
       placeholder="Write a new todo here" />

Modify button element to call own made addTodo function when button is clicked.

App.js - ToDoFormAndList()
1
<input type='button' value='Add' onClick={addTodo}/>

And create addTodo function inside ToDoFormAndList function. Add a new item to items and clears itemText state.

App.js - ToDoFormAndList()
1
2
3
4
5
// add a new todo
const addTodo = () => {
  console.log(`Add a new todo ${newTodoText}`);
  setNewTodoText('');
}

Check that your button event handling is working (open your browser inspector/console).

Next modify your code and use axios to send a new todo request to server side.

App.js - ToDoFormAndList()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// add a new item
const addTodo = async () => {
  //console.log(`Add a new todo ${newTodoText}`);
  const data = { 
    'text': newTodoText,
    'date': moment().format() // "2023-01-24T12:07:58+02:00"
  }
  const response = await axios.post('http://localhost:3000/api/todos', data);
  console.log(response.data);
  setNewTodoText('');
}

Use your React app UI and add a new todo. Check response from your browser inspector.

Image 12

Finally modify your code to add a new todo to your todos and it will be automatically be visible in the UI because you will change the state of your todos hooks.

App.js - ToDoFormAndList()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// add a new item
const addTodo = async () => {
  //console.log(`Add a new todo ${newTodoText}`);
  const data = { 
    'text': newTodoText,
    'date': moment().format() // "2023-01-24T12:07:58+02:00"
  }
  const response = await axios.post('http://localhost:3000/api/todos', data);
  //console.log(response.data);
  setTodos([...todos, response.data.data])
  setNewTodoText('');
}

Add a few todos. Remember check those are also stored in your MongoDB database.

Image 13

Remove a todo

Add a new span item's onClick event to call removeTodo function with selected todo._id. Note that these _id's are generated automatically in MongoDB database.

App.js - ToDoFormAndList()
1
2
3
4
5
6
// create li-elements from todos data
const todoItems = todos.map((todo,index) =>
  <li key={index}>
    { todo.text } : { moment(todo.date).format('DD.MM.YYYY') } <span onClick={() => removeTodo(todo._id)}> x </span>
  </li>
);

Now todo list items will show a x char which user can click to remove a todo.

Add removeTodo function to your application. This function will use axios to send delete method request to your server side REST API. After that a todo will be filtered away from todos state collection and a new todos state will be set and React will render component again.

App.js - ToDoFormAndList()
1
2
3
4
5
6
7
8
9
// remove todo
const removeTodo = async (id) => {
  console.log(id)
  const response = await axios.delete(`http://localhost:3000/api/todos/${id}`);
  // filter/remove todo item from todos state array
  let newTodos = todos.filter(todo => todo._id !== id)
  // modify todos state -> will render component again
  setTodos(newTodos)
}

Save your files and try to remove/add a few todos.

Image 14

Final words

This exercise was a just a quick demo which shows how to create fullstack application with React, Node, Express and Mongo. This stack is called as a MERN stack.

You can modify this exercises to handle more features and add error checking. For example Mongoose validation is not used when a new todo is tried to save database.

Read More

Goals of this topic

Understand

  • Understand the basic of the FullStack application development