Write a todo list with Express and MongoDB

A todo list website is a good practice to learn a programing language or a framework. It shows you how to create, read, update and delete records. In this post we are going to use Express as our application framework and MongoDB as our data store.

Source

On github | Download | Live Demo

Functionalities

The followings are the functionalities this website should have.

  • Users do not need to login, we use cookie to remeber each user.
  • Users should be able to create, read, update and delete his/her todo list.

Installation

Development Environment

Before we start make sure you have installed node.js, Express and MongoDB. If you havn’t please follow the instruction in these posts.


node.js packages

ref : npm basic commands
  • Install Express
$ npm install express@3.4.7 -g

Also We use Mongoose as our ORM. You might think why we need a ORM that defines database schema when using a schema-less database? The thing is in most of the application we validate data, handle model relation. Mongoose does it really well for us. We will see how to install it later.

Steps

Using express command line tool to generate a project bootstrap

The default template engine is jade, we are going to use ejs in this example.
# pass -e for using ejs
$ express todo -e

create : todo
create : todo/package.json
create : todo/app.js
create : todo/public
create : todo/public/javascripts
create : todo/public/stylesheets
create : todo/public/stylesheets/style.css
create : todo/routes
create : todo/routes/index.js
create : todo/routes/user.js
create : todo/public/images
create : todo/views
create : todo/views/index.ejs

Add .gitignore file to project root

.DS_Store
node_modules
*.sock

Add ejs-locals and mongoose as dependencies

Express removed layout and partial support in 3.0+, so we need to add ejs-locals to get it back. Also, you need to add a layout.ejs to your view folder.

Edit package.json
{
  "name"         : "todo",
  "version"      : "0.0.1",
  "private"      : true,
  "dependencies" : {
    "express"    : "3.4.7",
    "ejs"        : "0.8.5",
    "ejs-locals" : "1.0.2",
    "mongoose"   : "3.8.1"
  }
}

Install dependencies

$ cd todo && npm install

Hello world

Start the express server and open your browser, go to 127.0.0.1:3000 and see the welcome page.
$ node app.js

Project structure

Let’s take a look the todo project structure generated by Express.
todo
|-- node_modules
|   |-- ejs
|   |-- ejs-locals
|   |-- express
|   `-- mongoose
|
|-- public
|   |-- images
|   |-- javascripts
|   `-- stylesheets
|       |-- style.css
|
|-- routes
|   `-- index.js
|
|-- views
|   |-- index.ejs
|   `-- layout.ejs
|
|-- .gitignore
|
|-- app.js
|
`-- package.json
  • node_modules
    • all the project dependencies.
  • public
    • assets
  • routes
    • actions, including business logics.
  • views
    • action views, partials and layouts.
  • app.js
    • configs, middlewares, and dispatch routes.
  • package.json
    • dependencies configs.


MongoDB & Mongoose setup

If you use Ubuntu MongoDB starts after boot, if you use Mac you will have to start it by the following command.
$ mongod
Create a file call db.js and config your MongoDB and schema.
var mongoose = require( 'mongoose' );
var Schema   = mongoose.Schema;

var Todo = new Schema({
    user_id    : String,
    content    : String,
    updated_at : Date
});

mongoose.model( 'Todo', Todo );
mongoose.connect( 'mongodb://localhost/express-todo' );
Require it in app.js on the very top.
require( './db' );
Remove useless user route that was generated by Express and add layout support.
// mongoose setup
require( './db' );

var express = require( 'express' );
var routes  = require( './routes' );
var http    = require( 'http' );
var path    = require( 'path' );
var app     = express();
var engine  = require( 'ejs-locals' );

// all environments
app.set( 'port', process.env.PORT || 3001 );
app.engine( 'ejs', engine );
app.set( 'views', path.join( __dirname, 'views' ));
app.set( 'view engine', 'ejs' );
app.use( express.favicon());
app.use( express.logger( 'dev' ));
app.use( express.json());
app.use( express.urlencoded());
app.use( express.methodOverride());
app.use( app.router );
app.use( express.static( path.join( __dirname, 'public' )));

// development only
if ( 'development' == app.get( 'env' )) {
  app.use( express.errorHandler());
}

app.get( '/', routes.index );

http.createServer( app ).listen( app.get( 'port' ), function(){
  console.log( 'Express server listening on port ' + app.get( 'port' ));
} );

Change the project title

routes/index.js
exports.index = function ( req, res ){
  res.render( 'index', { title : 'Express Todo Example' });
};

Edit the index view

We need an input to add new todo item, here we use a POST form to send the data. Don’t forget to set the layout here.

views/index.ejs
<% layout( 'layout' ) -%>

<h1><%= title %></h1>
<form action="/create" method="post" accept-charset="utf-8">
  <input type="text" name="content" />
</form>

Create items and save

routes/index.js
First we have to require mongoose and the Todo model before we can use it.
var mongoose = require( 'mongoose' );
var Todo     = mongoose.model( 'Todo' );
Redirect the page back to index after the record is created.
exports.create = function ( req, res ){
  new Todo({
    content    : req.body.content,
    updated_at : Date.now()
  }).save( function( err, todo, count ){
    res.redirect( '/' );
  });
};
Add this create action to routes.
app.js
// add this before app.use( express.json());
app.use( express.bodyParser());
// add this line to the routes section
app.post( '/create', routes.create );

Show todo lists

We now have the ability to create todo items, let’s show them on the index page.
routes/index.js
// query db for all todo items
exports.index = function ( req, res ){
  Todo.find( function ( err, todos, count ){
    res.render( 'index', {
      title : 'Express Todo Example',
      todos : todos
    });
  });
};
views/index.ejs
// use a loop to show all todo items at the very bottom.
<% todos.forEach( function( todo ){ %>
  <p><%= todo.content %></p>
<% }); %>

Delete items

Add a delete link beside the todo item.
routes/index.js
// remove todo item by its id
exports.destroy = function ( req, res ){
  Todo.findById( req.params.id, function ( err, todo ){
    todo.remove( function ( err, todo ){
      res.redirect( '/' );
    });
  });
};
views/index.ejs
// add a delete link in the loop
<% todos.forEach( function ( todo ){ %>
  <p>
    <span>
      <%= todo.content %>
    </span>
    <span>
      <a href="/destroy/<%= todo._id %>" title="Delete this todo item">Delete</a>
    </span>
  </p>
<% }); %>
Add this delete action to routes.
app.js
// add this line to the routes section
app.get( '/destroy/:id', routes.destroy );

Edit item

When click on the text of the todo item, turn it into an text input.
routes/index.js
exports.edit = function ( req, res ){
  Todo.find( function ( err, todos ){
    res.render( 'edit', {
        title   : 'Express Todo Example',
        todos   : todos,
        current : req.params.id
    });
  });
};
Edit view is basically the same as index, the only difference is it gives a text input for the specific todo item.
views/edit.ejs
<% layout( 'layout' ) -%>

<h1><%= title %></h1>
<form action="/create" method="post" accept-charset="utf-8">
  <input type="text" name="content" />
</form>

<% todos.forEach( function( todo ){ %>
  <p>
    <span>
      <% if( todo._id == current ){ %>
      <form action="/update/<%= todo._id %>" method="post" accept-charset="utf-8">
        <input type="text" name="content" value="<%= todo.content %>" />
      </form>
      <% }else{ %>
        <a href="/edit/<%= todo._id %>" title="Update this todo item"><%= todo.content %></a>
      <% } %>
    </span>
    <span>
      <a href="/destroy/<%= todo._id %>" title="Delete this todo item">Delete</a>
    </span>
  </p>
<% }); %>
Wrap the todo content with a link that links to edit action.
views/index.ejs
<% layout( 'layout' ) -%>

<h1><%= title %></h1>
<form action="/create" method="post" accept-charset="utf-8">
  <input type="text" name="content" />
</form>

<% todos.forEach( function ( todo ){ %>
  <p>
    <span>
      <a href="/edit/<%= todo._id %>" title="Update this todo item"><%= todo.content %></a>
    </span>
    <span>
      <a href="/destroy/<%= todo._id %>" title="Delete this todo item">Delete</a>
    </span>
  </p>
<% }); %>
Add this edit action to routes.
app.js
// add this line to the routes section
app.get( '/edit/:id', routes.edit );

Update item

Add an update action so the todo item can be updated with user input text.
routes/index.js
// redirect to index when finish
exports.update = function ( req, res ){
  Todo.findById( req.params.id, function ( err, todo ){
    todo.content    = req.body.content;
    todo.updated_at = Date.now();
    todo.save( function ( err, todo, count ){
      res.redirect( '/' );
    });
  });
};
Add this update action to routes.
app.js
// add this line to the routes section
app.post( '/update/:id', routes.update );

Sorting items

The todo items now follow the order by the oldest one on the top however we would like the latest one on the top.
routes/index.js
exports.index = function ( req, res ){
  Todo.
    find().
    sort( '-updated_at' ).
    exec( function ( err, todos ){
      res.render( 'index', {
          title : 'Express Todo Example',
          todos : todos
      });
    });
};

exports.edit = function ( req, res ){
  Todo.
    find().
    sort( '-updated_at' ).
    exec( function ( err, todos ){
      res.render( 'edit', {
          title   : 'Express Todo Example',
          todos   : todos,
          current : req.params.id
      });
    });
};

Multiple users

For now all the users get the same result when visiting this site, we are going to use cookie to store different user information so that ever user gets his own todo list. Express has build-in cookie support, just add a line in app.js. Also we need to generate a uniqe ID for user and a currentUser middleware to get the current user id.

app.js
// mongoose setup
require( './db' );

var express = require( 'express' );
var routes  = require( './routes' );
var http    = require( 'http' );
var path    = require( 'path' );
var app     = express();
var engine  = require( 'ejs-locals' );

// all environments
app.set( 'port', process.env.PORT || 3001 );
app.engine( 'ejs', engine );
app.set( 'views', path.join( __dirname, 'views' ));
app.set( 'view engine', 'ejs' );
app.use( express.favicon());
app.use( express.logger( 'dev' ));
app.use( express.cookieParser());
app.use( express.bodyParser());
app.use( express.json());
app.use( express.urlencoded());
app.use( express.methodOverride());
app.use( routes.current_user );
app.use( app.router );
app.use( express.static( path.join( __dirname, 'public' )));

// development only
if( 'development' == app.get( 'env' )){
  app.use( express.errorHandler());
}

// Routes
app.get(  '/',            routes.index );
app.post( '/create',      routes.create );
app.get(  '/destroy/:id', routes.destroy );
app.get(  '/edit/:id',    routes.edit );
app.post( '/update/:id',  routes.update );

http.createServer( app ).listen( app.get( 'port' ), function (){
  console.log( 'Express server listening on port ' + app.get( 'port' ));
});
routes/index.js
var utils    = require( '../utils' );
var mongoose = require( 'mongoose' );
var Todo     = mongoose.model( 'Todo' );

exports.index = function ( req, res, next ){
  var user_id = req.cookies ?
    req.cookies.user_id : undefined;

  Todo.
    find({ user_id : user_id }).
    sort( '-updated_at' ).
    exec( function ( err, todos ){
      if( err ) return next( err );

      res.render( 'index', {
          title : 'Express Todo Example',
          todos : todos
      });
    });
};

exports.create = function ( req, res, next ){
  new Todo({
      user_id    : req.cookies.user_id,
      content    : req.body.content,
      updated_at : Date.now()
  }).save( function ( err, todo, count ){
    if( err ) return next( err );

    res.redirect( '/' );
  });
};

exports.destroy = function ( req, res, next ){
  Todo.findById( req.params.id, function ( err, todo ){
    var user_id = req.cookies ?
      req.cookies.user_id : undefined;

    if( todo.user_id !== req.cookies.user_id ){
      return utils.forbidden( res );
    }

    todo.remove( function ( err, todo ){
      if( err ) return next( err );

      res.redirect( '/' );
    });
  });
};

exports.edit = function( req, res, next ){
  var user_id = req.cookies ?
      req.cookies.user_id : undefined;

  Todo.
    find({ user_id : user_id }).
    sort( '-updated_at' ).
    exec( function ( err, todos ){
      if( err ) return next( err );

      res.render( 'edit', {
        title   : 'Express Todo Example',
        todos   : todos,
        current : req.params.id
      });
    });
};

exports.update = function( req, res, next ){
  Todo.findById( req.params.id, function ( err, todo ){
    var user_id = req.cookies ?
      req.cookies.user_id : undefined;

    if( todo.user_id !== user_id ){
      return utils.forbidden( res );
    }

    todo.content    = req.body.content;
    todo.updated_at = Date.now();
    todo.save( function ( err, todo, count ){
      if( err ) return next( err );

      res.redirect( '/' );
    });
  });
};

// ** express turns the cookie key to lowercase **
exports.current_user = function ( req, res, next ){
  var user_id = req.cookies ?
      req.cookies.user_id : undefined;

  if( !user_id ){
    res.cookie( 'user_id', utils.uid( 32 ));
  }

  next();
};

Error handling

To deal with errors we have to add a next parameter in every action, and once error occuers poass it to the next middleware.

routes/index.js
... function ( req, res, next ){
  // ...
};

...( function( err, todo, count ){
  if( err ) return next( err );

  // ...
});

Run application

$ node app.js

We have done most of the job, the only difference in the souce is there are extra styles to make it look a little nicer. let’s start the server and try out this todo app, enjoy it :)

Related posts