Skip to content

M09 - Full Stack Example - Todo App

Introduction

This material describes the principles of creating a Todo Application. The application implements the functionality of a simple fullstack application. The user can add and delete todos. The todos are stored in the MongoDB database and are visible on the web page.

The image below shows an empty to-do list, when the page has been accessed for the first time.

Image 01

The image below shows a few added tasks.

Image 02

Application architecture

The image below shows the architecture of the application.

  • Application UI uses HTML/CSS and JavaScript.
  • 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.

Image 03

Database

This example application uses MonboDB database located in the Mongo Atlas cloud service. When creating this example application, you must have a username and password to the service. Also find out the connection address of your service database. You will need it later when following this material.

Image 04

Look more information from MongoDB materials.

Server side programming

On the server side, Node.js programming and the necessary npm packages are used for programming.

Project

Create a new Node.js project

1
2
3
mkdir todo
cd todo
npm init

npm packages

Install following npm packages to your project.

1
2
3
4
npm install express
npm install mongoose
npm install cors
npm install --save-dev nodemon

Note

Read more about Express cors middleware.

Express application

The implementation starts by making a simple Express application using Node.js to the index.js file defined in the project. The goal is to get the server to respond to a single GET request.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
const express = require('express') 
const cors = require('cors')
const app = express()
const port = 3000

// cors - allow connection from different domains and ports
app.use(cors())

// convert json string to json object (from request)
app.use(express.json())

// mongo here...

// todos-route
app.get('/todos', (request, response) => {
  response.send('Todos')
})

// app listen port 3000
app.listen(port, () => {
  console.log('Example app listening on port 3000')
})

Now the application should respond to the following request in the web browser: http://localhost:3000/todos, returning only the Todos string to the visible body of the browser.

Connection to MongoDB database

Add a needed connection to MongoDB. The project uses the Mongoose library.

Add the programming below to // Mongo here... comment in the programming above. Remember to use your own MongoDB database address. MongoDB is used with a Mongoose. A possible successful or unsuccessful connections is visible in the console.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const mongoose = require('mongoose')
const mongoDB = 'mongodb+srv://... YOUR OWN MONGO CONNECTION STRING HERE'
mongoose.connect(mongoDB, {useNewUrlParser: true, useUnifiedTopology: true})

const db = mongoose.connection
db.on('error', console.error.bind(console, 'connection error:'))
db.once('open', function() {
  console.log("Database test connected")
})

// Mongoose Scheema and Model here...

Mongoose Scheema and Model

The Todo database will be used through Todo model. Mongoose's Schema will be used to create a Mongoose Model.

Add the programming below to // Mongoose Schema and Model here... comment in the programming above. In programming, a new Schema and the model that uses it are created. Todo documents are saved in the todos collection of the opened MongoDB database.

1
2
3
4
5
6
7
8
9
// scheema
const todoSchema = new mongoose.Schema({
  text: { type: String, required: true } 
})

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

// Routes here...

Routes, Todos API

The last trick is to create/add the routes provided by the server, i.e. to form Todo's API, which is provided for user interface programming. Use VS Code's RestClient or Postman and test the functionality of all defined routes before starting client-side programming.

Add the routes below to the // Routes here... comment section of the previous programming.

POST

POST route adds a new Todo document to the database. The todo text is obtained from the client application via the request.body object. Next, a new Todo object is created using the Mongoose model. Finally, the created todo object is stored in the MongoDB database and the stored object is returned back to the client application.

1
2
3
4
5
6
7
8
app.post('/todos', async (request, response) => {
  const { text } = request.body
  const todo = new Todo({
    text: text
  })
  const savedTodo = await todo.save()
  response.json(savedTodo)  
})

Functional testing of the route via VS Code is visible below. Note that the server also returns the _id of the todo in the MongoDB database in the todo object. This _id value will be used later to remove the todo.

Create your own post_todo.rest file (below) and send request to server side from VS Code.

Image 05

Note

Remember check that the saving a new todo has successfully done in the MongoDB database.

GET

GET route returns all the Todo documents from the database. Next, add a get route that reads the MongoDB database using the Todo Mongoose model. An empty object {} is used as the search condition, thus all objects are returned from the base to the calling program.

1
2
3
4
app.get('/todos', async (request, response) => {
  const todos = await Todo.find({})
  response.json(todos)
})

Functional testing via VS Code is visible in below image.

Image 06

GET

GET route (parametric): returns the Todo document according to the id from the database. Next, add a get route that uses the id value of the todo object when specifying the route. This unique id value is searched for in the MongoDB database using the findById function. Either a found todo object or a 404 status value (not found) is returned to the calling program.

1
2
3
4
5
app.get('/todos/:id', async (request, response) => {
  const todo = await Todo.findById(request.params.id)
  if (todo) response.json(todo)
  else response.status(404).end()
})

Below you can see the functional testing of the route via VSCode.

Image 07

Note

Remember also try an id value that is not in use in your MongoDB todo collection. Note the returned 404 Not Found value and the Content-Length value of zero.

Image 08

DELETE

DELETE route deletes the Todo document according to the id from the database. The id value is passed as a route parameter. The corresponding todo object is retrieved from the MongoDB database using the findByIdAndRemove function of the Todo model. The route returns a deleted todo object or a 404 status code (not found) to the calling program.

Use below code if mongoose 8 or above version is used. Check versio number from package.json file.

1
2
3
4
5
6
7
8
app.delete('/todos/:id', async (request, response) => {
  const doc = await Todo.findById(request.params.id);
  if (doc) { 
    await doc.deleteOne()
    response.json(doc)
  }
  else response.status(404).end()
})

Use below code if mongoose version below 8 is used.

1
2
3
4
5
app.delete('/todos/:id', async (request, response) => {
  const deletedTodo = await Todo.findByIdAndRemove(request.params.id)
  if (deletedTodo) response.json(deletedTodo)
  else response.status(404).end()
})

Below you can see the functional testing of the route via VSCode.

Image 09

UI with HTML/CSS and JavaScript

The user interface must be built using HTML, CSS and JavaScript technologies.

HTML

Create a new folder and index.html file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Full Stack Example Application</title>
  <link rel="stylesheet" href="styles.css">
  <script src="code.js"></script>
</head>
<body onload="init()">
  <div id="container">
    <h1>Todo List</h1>
    <input type="text" id="newTodo"/>
    <button onclick="addTodo()">Add</button>
    <ul id="todosList"></ul>
    <p id="infoText"></p>
  </div>
</body>
</html>

Note a folliwing from above html file

  • own made styles.css will be used (created later)
  • own made code.js will be used (created later)
  • will call init() function when a HTML page is loaded
  • new todo text can be found via newTodo id
  • pressing the Add button calls the addTodo function
  • todo items are displayed in todosList
  • possible messages can be displayed in the infoText

CSS

You can use the style definitions below, or create your own even nicer personal styles for your own todo application.

Create a new styles.css file.

 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
body {
  font-family: 'Oswald', sans-serif;
}

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

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

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

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

button {
  border: 1px solid #ccc;
  margin-left: 15px;
  padding: 5px 15px;
  font-size: 15px;
  cursor: pointer;
}

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

JavaScript

Next, the programming of the user interface must be carried out. Let's start the task by implementing the init function, which is called when the entire HTML code has been rendered into the body of the web browser, i.e. the onload event of the body has taken place. See previous HTML code and line 9.

In the programming below, the infoText id is retrieved from the HTML and Loading todos, please wait... text will be visible.

1
2
3
4
5
function init() {
  let infoText = document.getElementById('infoText')
  infoText.innerHTML = 'Loading todos, please wait...'
  // loadTodos()
}

Now you will need to run the client-side programming (HTML, CSS and JavaScript) through the right server, so that the data can be downloaded successfully to the Node.js application. One good option is to install the LiveServer add-on for Visual Studio Code. Of course, you can always transfer files after changes to e.g. the student server, but it's tedious in the middle of programming the application.

Install the LiveServer add-on and start your application by pressing the "Go Live" button from the Visual Studio Code view (when writing the material, the button is visible at the bottom right of the bottom bar). Or right-click when the HTML file is open and select "Open with Live Server".

Now you should be able to see the UI of the application, with the text related to the data downloading visible.

Image 10

Next start downloading data from the server. In addition to the JavaScript programming, add the loadTodo function below and uncomment the function call from the previous programming, so that the function is called from the init function.

1
2
3
4
5
6
7
async function loadTodos() {
  // change ip and port if needed
  let response = await fetch('http://localhost:3000/todos')
  let todos = await response.json()
  console.log(todos)
  //showTodos(todos)
}

Save and refresh the browser view.

The image below shows todos objects loaded to the web browser console. Please note that you may see a different number of todos (depending on how many of them you have added to the database).

Image 11

In the application, new LI elements are made to represent one todo item. In the implementation, it is necessary to make these LI elements visible after the application is started (i.e. all todo objects are retrieved from the base) and whenever a new todo is added to the application. Thus, this same "routine" is needed in at least two parts of the application. Therefore, you should make your own function and call it whenever necessary.

Add the following createTodoListItem function to your JavaScript. The function takes a new todo object as a parameter, i.e. an object that always contains the text and id of the todo.

 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
function createTodoListItem(todo) {
  // create a new  LI element
  let li = document.createElement('li')
  // create a new id attribute
  let li_attr = document.createAttribute('id')
  // add todo id value to attribute
  li_attr.value= todo._id
  // add attribute to LI element
  li.setAttributeNode(li_attr)
  // add a new text node with todo text
  let text = document.createTextNode(todo.text)
  // add text node to LI element
  li.appendChild(text)
  // create a new SPAN element, x char -> delete todo
  let span = document.createElement('span')
  // create a new attribute
  let span_attr = document.createAttribute('class')
  // add delete value (look css)
  span_attr.value = 'delete'
  // add attribute to SPAN element 
  span.setAttributeNode(span_attr)
  // create a text node with x text
  let x = document.createTextNode(' x ')
  // add text node to SPAN element 
  span.appendChild(x)
  // add event listener to SPAN element, onclick event call removeTodo function
  span.onclick = function() { removeTodo(todo._id) }
  // add SPAN element to LI element
  li.appendChild(span)
  // return created LI element 
  // will be following formula: <li>Call Esa!<span class="remove">x</span></li>
  return li
}

Note

Look above code comments very carefully!

That's it, now we have a function that can be used to create a new LI element in the todosList in the user interface whenever necessary with the data of the todo object. Function will be used in the next showTodos function.

In the earlier loadTodos function, uncomment the showTodos function call so that the function below is called.

Add below showTodos function to your JavaScript file. At first, the programming retrieves the todosList and infoText items from the HTML file and updates their values according to the situation.

If todos objects have been downloaded from the server, they are created/showed using the forEach loop. In each round of iteration, a new LI element is created using the above createTodoListItem function. The created LI elements are connected (as children) to the UL element todosList in the user interface.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function showTodos(todos) {
  let todosList = document.getElementById('todosList')
  let infoText = document.getElementById('infoText')
  // no todos
  if (todos.length === 0) {
    infoText.innerHTML = 'No Todos'
  } else {    
    todos.forEach(todo => {
        let li = createTodoListItem(todo)        
        todosList.appendChild(li)
    })
    infoText.innerHTML = ''
  }
}

Save codes and check how the situation looks in the application in the browser. All todo objects should be visible in the user interface.

Image 12

Pressing the Add button in the UI calls the addTodo() function (check your HTML). Next, add the addTodo() function below to your JavaScript programming.

The programming looks for the todo text in the user interface and uses fetch to communicate todos route on the server using the POST method. This can be achieved by using the object with the fetch command. Note that we need to convert the JSON data into a string using the JSON.stringify function and specifying the data type to be sent as JSON. The server reads data from request.body and we attach the data to be sent to it (check the server-side programming made earlier for the POST route).

User interface programming is waiting for feedback from the server. After receiving it, a new LI element is created and added to the user interface.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
async function addTodo() {
  let newTodo = document.getElementById('newTodo')
  const data = { 'text': newTodo.value }
  const response = await fetch('http://localhost:3000/todos', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(data)
  })
  let todo = await response.json()
  let todosList = document.getElementById('todosList')
  let li = createTodoListItem(todo)
  todosList.appendChild(li)

  let infoText = document.getElementById('infoText')
  infoText.innerHTML = ''
  newTodo.value = ''
}

Test and add a few Todos.

Image 13

The last trick is to add a delete task to the app. The x character inside the LI element is defined to call the removeTodo function. See the previous implementation of the createTodoListItem function.

Todo API interface on the server is called with the DELETE method to the todos route with the parameter (todo id). First delete the data from the server/database and after that the LI element also deleted from the user interface (it has a unique id because it is also unique in the MongoDB database). The user interface displays the text No Todos, if all tasks have been deleted.

Add below code to your app.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
async function removeTodo(id) {
  const response = await fetch('http://localhost:3000/todos/'+id, {
    method: 'DELETE'
  })
  let responseJson = await response.json()
  let li = document.getElementById(id)
  li.remove()

  let todosList = document.getElementById('todosList')
  if (!todosList.hasChildNodes()) {
    let infoText = document.getElementById('infoText')
    infoText.innerHTML = 'No Todos'
  }
}

Try delete a few of your Todos.

Image 14

Image 15

Summary

The todo application presented in this material implements the principles of a simple fullstack application. It includes the user interface, connection to the server, server-side programming and data storage in the database. The application is very simple and does not contain e.g. managing error situations or editing tasks.

Another good addition would be to implement users for the todos as well. For example, you could register for the application and each user would have the opportunity to add/change/delete only their own todos.

You can think about these things in your own practice work.