DEV Community

Samuel James
Samuel James

Posted on • Updated on

Build a Todo App with Node.Js, ExpressJs, MongoDB and VueJs – Part 1

Hint: This is what the final result would look like.

In this tutorial, we will build the famous todo application with Node.Js using the ExpressJs framework and MongoDB. Did I forget to tell you? The app will be API centric and full-stack :).

In a nutshell, if you want to learn how to build APIs with Node.Js, you have come to the right place.

Now you can grab a bottle of beer and let's get our hands dirty.

What is ExpressJs?

ExpressJs simply put, it's a web framework for Node.Js - stolen from the official docs. Taylor Otwell (Laravel's creator) once said, "Developers build tools for developers". ExpressJs was built for developers to simplify Node APIs.

What is MongoDB?

MongoDB is a NoSQL database. It's completely document-oriented. A NoSQL database allows you to store data in the form of JSON and any formats. If you want to learn more about MongoDB, I have also written a post on MongoDB here.

Defining Todo APIs

I like to begin by defining my APIs. The table below shows what APIs we need to create and what each does.

Method Path Description
GET /todos Get all todos items
GET /todos/:id Get one todo item
POST /todos Create a new todo item
PUT /todos/:id Update a todo item
DELETE /todos/:id Delete a new todo item

Having defined our APIs, let's delve in to the project directories.

Project Directories

You are probably going to get a shock of your life when I tell ya we need not more than 5 files with relatively few lines of code to build this backend. Yes! It is that simple. This how we roll man :).

You should create a project folder at this point to house all the source code for the backend part of this application. I'll call my backend. Feel free to use the backend as your directory. It's not patented.:)

To keep things simple, and of course, we should not throw away flexibility at the sight of simplicity. We'll create just 4 folders in the directory you created above.

  1. config: This contains configuration files for the app.
  2. models: This houses our entity (Todo data structure).
  3. repositories: A repository adds an abstraction layer on the data access. You can read more about why it's important to have this layer here
  4. routes: A route is an entry to your application. This folder contains a file that defines what should happen when a user accesses a particular route.
├── config
├── models
├── respositories
└── routes

What you will need to install

  1. You will need Node
  2. You will need to install MongoDB

Application Packages

This app depends on a couple of packages and will use npm to install them. Navigate to the project directory you just created and create a package.json file with the content below.

{
    "name": "node-todo",
    "version": "1.0.0",
    "description": "Simple todo application.",
    "main": "server.js",
    "author": "Samuel James",
    "scripts": {
        "build": "webpack",
        "start": "node app.js"
    },
    "dependencies": {
        "cookie-parser": "~1.4.4",
        "cors": "^2.8.5",
        "debug": "~2.6.9",
        "express": "~4.16.1",
        "http-errors": "~1.6.3",
        "jade": "~1.11.0",
        "mongoose": "^5.7.3",
        "morgan": "~1.9.1"
    }
}

Run npm install to install the dependencies. Let's go ahead and define config parameters our app needs.

Config file

We'll define database connection URL and the port the app will listen on in config/Config.js file as follows:

//config/Config.js

module.exports = {
  DB: process.env.MONGO_URL ? process.env.MONGO_URL : 'mongodb://localhost:27017/todos',
  APP_PORT: process.env.APP_PORT ? process.env.APP_PORT : 80,
};

In the config/Config.js, we set DB to environment variable MONGO_URL if defined, otherwise defaults to mongodb://localhost:27017/todos. We also did the same with APP_PORT.

Todo Model

Model is an object representation of data in the database. So, Let's create a file models/Todo.js with the content:

//models/Todo.js

const mongoose = require('mongoose');

const { Schema } = mongoose;

// Define schema for todo items
const todoSchema = new Schema({
  name: {
    type: String,
  },
  done: {
    type: Boolean,
  },
});

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

module.exports = Todo;

If you notice, we use mongoose for schema definition, right? Mongoose is an official MongoDB library built for manipulating MongoDB databases in Node. In the structure, I have defined name and done.

name: This is the name of the todo item. We define it as string. For example, "I'm going for swimming by 3pm"
done: Todo item status which is boolean. If the todo item is still pending, its value will be false.

Now, we'll create a todo repository.

Repositories

I like to see repository as a strategy for abstracting data access. I became a big fan when this pattern saved me from heavy refactoring when I switched to a new datastore in one of my projects. It helps you decouple your project and reduces duplication in your code. Here is an interesting article I recommend you read on this pattern.

That said, create a file repositories/TodoRepository.js as:

//repositories/TodoRepository

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

class TodoRepository {

  constructor(model) {
    this.model = model;
  }

  // create a new todo
  create(name) {
    const newTodo = { name, done: false };
    const todo = new this.model(newTodo);

    return todo.save();
  }

  // return all todos

  findAll() {
    return this.model.find();
  }

  //find todo by the id
  findById(id) {
    return this.model.findById(id);
  }

    // delete todo
  deleteById(id) {
    return this.model.findByIdAndDelete(id);
  }

  //update todo
  updateById(id, object) {
    const query = { _id: id };
    return this.model.findOneAndUpdate(query, { $set: { name: object.name, done: object.done } });
  }
}

module.exports = new TodoRepository(Todo);

With TodoRepository.js defined, let's go create todo routes.

Routes

Every web application has at least an entry point. A route in web apps is more like saying: "Hey Jackson when I ask ya for this, give me that". Same goes for our app, we'll define what URL users need to access to get certain results or trigger certain actions.

In this case, we want users to be able to perform create, read, update and delete (CRUD) operations on todo items.

Now, that you know what "routes" does, create routes/Routes.js file and the code below:

const express = require('express');

const app = express.Router();
const repository = require('../respositories/TodoRepository');

// get all todo items in the db
app.get('/', (req, res) => {
  repository.findAll().then((todos) => {
    res.json(todos);
  }).catch((error) => console.log(error));
});

// add a todo item
app.post('/', (req, res) => {
  const { name } = req.body;
  repository.create(name).then((todo) => {
    res.json(todo);
  }).catch((error) => console.log(error));
});

// delete a todo item
app.delete('/:id', (req, res) => {
  const { id } = req.params;
  repository.deleteById(id).then((ok) => {
    console.log(ok);
    console.log(`Deleted record with id: ${id}`);
    res.status(200).json([]);
  }).catch((error) => console.log(error));
});

// update a todo item
app.put('/:id', (req, res) => {
  const { id } = req.params;
  const todo = { name: req.body.name, done: req.body.done };
  repository.updateById(id, todo)
    .then(res.status(200).json([]))
    .catch((error) => console.log(error));
});
module.exports = app;

First, you would want users to get a list of all to-do items existing in the database, hence we defined a route (/all) that accepts a get request and returns a JSON object of todo items if successful.

Our users like to get items as well as store new items, we added a route to create new to-do items. It accepts a post request.
When Mr. A makes a post request to route (/add), a new to-do item is created in the database.

Once a todo-item is completed, we also want users to be able to mark it as done. To do this, one must know what item a user intends to mark as done in the first place. So, we defined an 'update route' with a route parameter which is the ID of the item to update.

Server File

Having defined all routes that our application needs, it is time to create an entry file which is the main file to run our project.

At the project root folder, create a app.js file and update its content with this:

//app.js

const createError = require('http-errors');
const express = require('express');
const path = require('path');
const mongoose = require('mongoose');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const cors = require('cors')

const config = require('./config/Config');

const routes = require('./routes/Routes');

const app = express();

mongoose.connect(config.DB, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});

app.use(cors());  //enable cors

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/todos', routes);

// catch 404 and forward to error handler
app.use((req, res, next) => {
  next(createError(404));
});

// error handler
app.use((err, req, res) => {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

app.listen(config.APP_PORT); // Listen on port defined in environment


module.exports = app;

We required:

  • Express Js

  • morgan - a logging middleware for Node

  • path - a package for working with file and directory

  • mongoose - a package for working with MongoDB

  • body-parser - a body parsing middleware

We set our app to listen on the port set in config/Config.js. We also defined it to use routes defined in routes/Routes.js and prefix with todos.

Finally, your directory structure should look like this:

├── app.js
├── config
│   └── Config.js
├── models
│   └── Todo.js
├── package-lock.json
├── package.json
├── respositories
│   └── TodoRepository.js
└── routes
    └── Routes.js

To start the application, navigate to project root and run:

npm start

Let's create a new todo item

$ curl -H "Content-Type: application/json" -X POST -d '{"name":"Going Shopping"}' http://localhost/todos

{"__v":0,"name":"Going Shopping","done":false,"_id":"5a6365a39a2e56bc54000003"}

Get all todo items

$ curl  http://localhost/todos

[{"_id":"5a6365a39a2e56bc54000003","name":"Doing Laundry","done":false,"__v":0},{"_id":"5a6366039a2e56bc54000004","name":"Going Shopping","done":false,"__v":0}]

Done building? Check out Part 2.

Get the source code here

Top comments (26)

Collapse
 
cyberrob profile image
Pek Bun Ong • Edited

Hi Samuel,thanks for the tutorial!

I clone from your repo and run on my mac with node v8.9.4.

After run node server.js, 127.0.0.1:4000 works fine. But running curl command like your:

$ curl -H "Content-Type: application/json" -X POST -d '{"name":"Going Shopping"}' localhost:4000/api/add

the server just hanging there not responding anything:

Note: Unnecessary use of -X or --request, POST is already inferred.

  • Trying 127.0.0.1...
  • TCP_NODELAY set
  • Connected to 127.0.0.1 (127.0.0.1) port 4000 (#0) > POST /api/add HTTP/1.1 > Host: 127.0.0.1:4000 > User-Agent: curl/7.54.0 > Accept: / > Content-Type: application/json > Content-Length: 25 >
  • upload completely sent off: 25 out of 25 bytes

After Ctrl+C, server showed:
node server.js
App listening on port 4000
POST /api/add - - ms - -

Could you help me out here? Thank you.

Collapse
 
_akash_j_ profile image
Akash Jobanputra • Edited

I was facing the same issue, after hours of searching, I found that downgrading the version of mongoose to 4.7.6 works.
I used below command to downgrade the mongoose module, and then the API worked perfectly.

npm install mongoose@4.7.6

Collapse
 
zsofiaboldizsar profile image
Zsofia Boldizsar • Edited

Thanks for sharing this, Akash. I should have looked at the comments section in the first place. I had the same issue, but with your tip, I managed to solve it.

Collapse
 
tvviem profile image
Triệu Vĩnh Viêm

We can use Postman app to send request form get/post. You will receive response data.

Collapse
 
abiodunjames profile image
Samuel James • Edited

Hi Pek,

My bad! I'm just seeing this. I apologize for my late response.
Hope downgrading solves your problem as suggested by @Akash Jobanputra

Collapse
 
unika profile image
unika • Edited

Hi Samuel!

I followed your first part of the tutorial, but my curl request didn't work.
And my app didn't work, but my IDE didn't produce me any mistakes...

UPDATE

You missed the code:

app.listen(port, (err) => {
if (err) {
return console.log('something bad happened', err)
}
console.log(server is listening on ${port})
})

My app works with it.

UPDATE

This: curl -H "Content-Type: application/json" -X POST -d '{"name":"Going Shopping"}' localhost:4000/api/add

Show me this:

Unexpected token ' in JSON at position 0

Please, could you help me with it?

Collapse
 
abiodunjames profile image
Samuel James • Edited

Hello Unika,

Sorry for my late response. Use this:

curl -X POST -H "Content-Type: application/json" \
-d "{\"name\":\"Going shopping\"}" http://localhost:4000/api/add

Collapse
 
sambenskin profile image
Sam Benskin

Good article, small suggestion if I may. The use of third party libraries like Morgan and so on is good but it would have been better to introduce them later on and just keep it super simple to begin with. Then as we're reading we can see the code build up from the simple to the more complex as you add in each part.

Other than that, a great read, thanks

Collapse
 
abiodunjames profile image
Samuel James

Oh my Gosh, I overlooked that.

Thanks, Sam. I'll put that into consideration in my next post.

Collapse
 
erikiva profile image
Natalia Vidal

Also the first error I got was that morgan could not be found..I've installed it and it's all working now but it isn't on the package.json nor does it say to install it anywhere.

Thread Thread
 
abiodunjames profile image
Samuel James

Sorry @Natalia, The article has been updated to include Morgan.

Collapse
 
hvdrew profile image
Andrew • Edited

Howdy.

Great job on the article! I thought I would make a few suggestions just in case they haven't been made:

  • Try to stay consistent with your error handling, there are different styles written all throughout this first part of the tutorial (you use err, then error, then err again). Keeping parameter names consistent and sticking to one particular pattern can help with readability and is in general a good practice.
  • If you are going to use an if/else in one route handler, consider using it in all. If you would rather stick to if/return patterns, do that instead. Just stay consistent!
  • The console.log after using app.listen could have been passed as a callback.
  • const and let would have both come in handy all throughout this project.
  • The route for the Index.html file is redundant. After setting the express.static directory you have already prepared it to serve that file on the root route.
  • It's probably easier for your readers if you group require statements by their type. For example, group all 3rd party dependencies, then group all file dependencies from your project, then start declaring things like the app instance. This makes it easier to follow for your readers.

Other than that the article was on point, great work. Best of luck to you!

Collapse
 
ashgale profile image
AshGale

Nice article Samuel.
Code seem easy enough to follow and get going to mission accomplished.

I would suggest to add at the start (even if git isn't needed, its good practice)
open the cmd line in your newly created folder and get node started with:

mkdir todo-app && cd todo-app && mkdir app && mkdir public
git init
npm init

Then modify the package.json to add the dependencies:

ps and to correct small typeo for creating the config.js file to Config.js

Collapse
 
ctuxboy profile image
Christophe Hollebeke • Edited

Hello,

It's my first steps with NodeJS. Trying this nice tutorial, but i have this error:

ubuntu@nodejs:~/todo-app$ node server.js
internal/modules/cjs/loader.js:583
throw err;
^

Error: Cannot find module './app/Config'
at Function.Module._resolveFilename (internal/modules/cjs/loader.js:581:15)
at Function.Module._load (internal/modules/cjs/loader.js:507:25)
at Module.require (internal/modules/cjs/loader.js:637:17)
at require (internal/modules/cjs/helpers.js:20:18)
at Object. (/home/ubuntu/todo-app/server.js:19:14)
at Module._compile (internal/modules/cjs/loader.js:689:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
at Module.load (internal/modules/cjs/loader.js:599:32)
at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
at Function.Module._load (internal/modules/cjs/loader.js:530:3)
at Function.Module.runMain (internal/modules/cjs/loader.js:742:12)
at startup (internal/bootstrap/node.js:279:19)
at bootstrapNodeJSCore (internal/bootstrap/node.js:696:3)

How can i solve this?

Collapse
 
abiodunjames profile image
Samuel James • Edited

Hi Christopher,
From the debug information, I could see that Config.js could not be imported. Please note that file names are case sensitive on Unix machines. Could you please double check Config.js is in the right case? I look forward to hearing from you :)

Collapse
 
ctuxboy profile image
Christophe Hollebeke

Hi Samuel,
Thanks a lot for your helpful answer. Yes that was the problem. Shame on me :-/

Ok, after that start the server: '$node server.js' but shows an error:

(node:2977) DeprecationWarning: current URL string parser is deprecated, and will be removed in a future version. To use the new parser, pass option { useNewUrlParser: true } to MongoClient.connect.

After searching the net, found a solution. I'm adding this line to server.js:
const dbconfig = { useNewUrlParser: true, };

And add the variable in this line:
mongoose.connect(config.DB, dbconfig)

Now it works :-)

I'm a webdesigner buildign WP websites, but i want to learn NodeJS. Having basic JS knowledge, so i'm an absolute NodeJS beginner.
Trying learning a lot with this useful and awesome examples.
And... sorry for my poor english.

Collapse
 
georgiagallant333 profile image
georgiagallant333

Hi Samuel! Thanks so much for this article. I am running into an issue where when I rune node server.js it says:

TypeError: Router.use() requires callback function but got a [object Object]
at /Users/georgia/todo-app/node_modules/express/lib/router/index.js:423:13
at Array.forEach ()
at Function.use (/Users/georgia/todo-app/node_modules/express/lib/router/index.js:419:13)
at Function.use (/Users/georgia/todo-app/node_modules/express/lib/application.js:193:14)
at Object. (/Users/georgia/todo-app/server.js:29:5)
at Module._compile (internal/modules/cjs/loader.js:721:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:732:10)
at Module.load (internal/modules/cjs/loader.js:620:32)
at tryModuleLoad (internal/modules/cjs/loader.js:560:12)
at Function.Module._load (internal/modules/cjs/loader.js:552:3)

And I'm not sure what is the problem. I was wondering if you have any thoughts? Thank you!!

Collapse
 
codehimanshu profile image
Himanshu Agrawal

The packages used are way too old, and doesnt support Promises. After updating the packages to :
express 4.17.1
method-override 3.0.0
mongoose 5.6.0
Everything got working

Collapse
 
thobyv profile image
Thoby V ijishakin

Nice article, so awesome and simple to follow

Collapse
 
abiodunjames profile image
Samuel James

Thanks for reading, Thoby

Collapse
 
thobyv profile image
Thoby V ijishakin

Welcome Sam! Was enlightening

Collapse
 
iamsanthi_naidu profile image
Santhi Lakshmi Yenumula

Hi Samuel, Thanks for sharing this. Can I have some deep explanation about bundle.js code.

Collapse
 
markjohnson303 profile image
Mark Johnson 👔

This is really awesome, thanks for putting it together! On to part 2 :) Maybe you could edit this post with a link to the second part?

Collapse
 
abiodunjames profile image
Samuel James

Thank you for this. It's now updated with the link to Part 2

Collapse
 
a7medmasry profile image
A7medmasry

When I refresh the page in a page that is directed to the main page is there a solution to this problem

Some comments may only be visible to logged-in visitors. Sign in to view all comments.