Building a Todo app with Node.js, MongoDB and Docker

This post will cover multiple topics related to Node.js, MongoDB and Docker. We will learn about the setup of Node.js and MongoDB with Docker through simple todo app.

Requirements

  • Docker and docker-composs
  • Node.js
  • MongoDB

Setup Node.js app structure

Create a directory with the name docker-todoapp and create server.js and package.js file. Open the package.json file and include the following dependencies that your todoapp required.

{
  "name": "docker-todoapp",
  "version": "1.0.0",
  "description": "Todo app with Node.js and mongodb",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "keywords": [],
  "author": "Jogesh",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.19.0",
    "ejs": "^2.6.2",
    "express": "^4.17.1",
    "moment": "^2.24.0",
    "mongoose": "^5.6.3"
  },
  "devDependencies": {
    "nodemon": "^1.19.1"
  }
}

We have used ejs template engine and express framework for our TODO app. Install all the dependencies with npm install. Make the directory structure according to the given screenshot.

As you see at the above screenshot, We are going to separate our models, views and config files. The server.js file is the main entry point to start our app.

MongoDB and Node.js connection

Create keys.js at /config directory. This file holds the connection url for MongoDB.

module.exports = {
    mongoProdURI: 'mongodb://mongo:27017/todoapp', 
};

Now open the server.js file and start editing the file.

const mongoose = require('mongoose');
const bodyParse = require('body-parser');
const app = require('express')();
......
......

// Database connection
const db = require('./config/keys').mongoProdURI;
mongoose
.connect(db, {useNewUrlParser: true})
.then(() => console.log(`Mongodb Connected`))
.catch(error => console.log(error));

.....

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`Server listening on port ${PORT}`);
});

Now start the application and check if the connection is working well or not. We have used the nodemon packages, this package will automatically track the files and will restart the app once it detects any change on any file. And if you check the package.json file, then I have already added the nodemon setting.

....
 "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
....

So to start the app we have to run npm run dev and check the log if everything working fine.

Setup view, routes and models

Time to create the model that will help to store, fetch and delete our data into the database. Create a file Todo.js at /models directory. We will only create 2 properties i.e. task and created_at. The task property will hold the name of the task and created_at will hold the datetime.

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const TodoSchema = new Schema({
    task: {
        type: String, 
        required: true
    }, 
    created_at: {
        type: Date, 
        default: Date.now()
    }
});

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

Now our model part is complete. Let’s jump into the View template. We are going to use the ejs template engine. Create a file todos.ejs at /views directory. We are using bootstrap for the design UI.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Todo App</title>
    <link rel="stylesheet" href="//stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
    <link rel="stylesheet" href="//stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
</head>
<body>
    <div class="app">
        <header>
            <nav class="navbar navbar-dark bg-primary">
                <span class="navbar-brand mb-0 h1">Todo App</span>
            </nav>
        </header>
        <div class="container">
            <div class="row">
                <div class="col-md-8 m-auto pt-4">
                    <form method="POST" action="/" autocomplete="off">
                        <div class="row">
                            <div class="col-12 form-group">
                                <label for="todo">Enter your task</label>
                                <input type="text" name="task" class="form-control"/>
                            </div>
                        </div>
                    </form>
                    <hr>
                    <div class="row">
                        <div class="col-12">
                            <% if(Object.keys(tasks).length > 0) { %>
                            <ul class="nav flex-column">
                                <% tasks.forEach(todo => { %>
                                    <li class="nav-item">
                                        <div class="d-flex justify-content-between py-1">
                                            <div class="d-flex flex-row">
                                                <div>
                                                    <%= todo.task %>
                                                    <p class="text-muted"><small><%= moment(todo.created_at).fromNow() %></small></p>
                                                </div>
                                            </div>
                                            <a href="javascript:;" onclick="this.children[0].submit()" class="text-danger">
                                                <form method="POST" action="/todo/destroy">
                                                    <input type="hidden" name="_key" value="<%= todo._id %>"/>
                                                </form>
                                                <i class="fa fa-trash-o"></i>
                                            </a>
                                        </div>
                                    </li>
                                <% }) %>
                            </ul>
                            <% } else { %>
                            <div class="text-center"><strong>Please add some task.</strong></div>
                            <% }%>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

Besides the design template, let’s talk about the implementation section.

<% if(Object.keys(tasks).length > 0) { %>
    <% tasks.forEach(todo => { %>
       <%= todo.task %>
    <% }) %>
<% } else { %>
....
<% } %>

First, we will check if we have any task assigned or not. If we found any task(s) then we have to print that task with there name and created date.

Let’s create the route file that will handle all the request and will provide the appropriate result. Create a front.js file at /routes directory.

const express = require('express');
const Todo = require('./../models/Todo');

const router = express.Router();

router.get('/', (req, res) => {

    Todo.find({}, (err, todos) => {

        res.render("todos", {
            tasks: (Object.keys(todos).length > 0 ? todos : {})
        });
    });
});


module.exports = router;

We do require the express framework because our routes request will be handled by the framework. And the Todo model that we already created. Our main route will check and render the TODO data to the todos.ejs file. We also have to add 2 more routes that will handle the POST request. One will help to add the task and the other will help to delete the particular task.

const express = require('express');
const Todo = require('./../models/Todo');

const router = express.Router();

// Home page route
router.get('/', (req, res) => {

    Todo.find({}, (err, todos) => {

        res.render("todos", {
            tasks: (Object.keys(todos).length > 0 ? todos : {})
        });
    });
});

// POST - Submit Task
router.post('/', (req, res) => {
    const newTask = new Todo({
        task: req.body.task
    });

    newTask.save()
    .then(task => res.redirect('/'))
    .catch(err => console.log(err));
});

// POST - Destroy todo item
router.post('/todo/destroy', (req, res) => {
    const taskKey = req.body._key;

    Todo.findOneAndRemove({_id: taskKey}, (err) => {

        if(err) console.log(err);
        res.redirect('/');
    });
});


module.exports = router;

Now open the server.js file and include the routes to the file.

const mongoose = require('mongoose');
const bodyParse = require('body-parser');
const app = require('express')();
const moment = require('moment');

// Fontend route
const FrontRouter = require('./routes/front');

// Set ejs template engine
app.set('view engine', 'ejs');

app.use(bodyParse.urlencoded({extended: false}));
app.locals.moment = moment;

// Database connection
const db = require('./config/keys').mongoProdURI;
mongoose
.connect(db, {useNewUrlParser: true})
.then(() => console.log(`Mongodb Connected`))
.catch(error => console.log(error));


app.use(FrontRouter);


const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`Server listening on port ${PORT}`);
});

Our app will look like the below screenshot.

Setup Dockerfile for Node.js and MongoDB

First, we need to install Node.js and MongoDB through docker-compose.yaml file. Let’s create a docker-compose.yaml file at our working directory i.e. docker-todoapp.

version: '3'
services:
  todoapp:
    container_name: todoapp
    restart: always
    build: ./
    volumes:
      - ./:/var/www/todoapp
    links:
      - mongo
    ports:
      - 3000:3000
    environment:
      - NODE_ENV=development
      - PORT=3000
  
  mongo:
    image: mongo
    container_name: mongo
    ports:
      - "27017:27017"

As you see in the YAML file, We have created two services. One is our todoapp and the second is MongoDB. We have used MongoDB image for our app. We have linked the mongo service with our todoapp service. Now we have to create a Dockerfile in the root of our working directory. In this Dockerfile we have to install the node 10 version.

FROM node:10-alpine

# Create app directory
WORKDIR /var/www/todoapp

# Bundle app source
COPY . .

# Install app dependencies
RUN npm install

EXPOSE 8080
CMD [ "node", "server.js" ]

After completing the above step, We have to build our app with docker-composer. So all the related dependencies will install will generate an image with our configuration.

sudo docker-compose build

Now the final step is to run our app. To run the app through docker-compose.

sudo docker-compose up -d

Resources