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@2.5.11 -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.
$ express todo -t ejs

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

Add .gitignore file to project root

.DS_Store
node_modules
*.sock

Add connect and mongoose as dependencies

Edit package.json
{
  "name"         : "todo",
  "version"      : "0.0.1",
  "private"      : true,
  "dependencies" : {
    "connect"  : "1.8.7",
    "express"  : "2.5.11",
    "ejs"      : "0.8.3",
    "mongoose" : "3.2.0"
  }
}

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
|   |-- 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 --dbpath /usr/local/db
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
require( './db' );
Move require routes after db config.
var express = require( 'express' );

var app = module.exports = express.createServer();

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

// Configuration
// remove methodOverride, add favicon, logger and move static middleware upper
app.configure( function (){
  app.set( 'views', __dirname + '/views' );
  app.set( 'view engine', 'ejs' );
  app.use( express.favicon());
  app.use( express.static( __dirname + '/public' ));
  app.use( express.logger());
  app.use( express.bodyParser());
  app.use( app.router );
});

app.configure( 'development', function (){
  app.use( express.errorHandler({ dumpExceptions : true, showStack : true }));
});

app.configure( 'production', function (){
  app.use( express.errorHandler());
});

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

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

app.listen( 3000, function (){
  console.log( 'Express server listening on port %d in %s mode', app.address().port, app.settings.env );
});

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.

views/index.ejs
<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 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
<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
<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
var express = require( 'express' );

var app = module.exports = express.createServer();

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

// move routes before middlewares
var routes = require( './routes' );

// Configuration
// add cookieParser and currentUser to middlewares
app.configure( function (){
  app.set( 'views', __dirname + '/views' );
  app.set( 'view engine', 'ejs' );
  app.use( express.favicon());
  app.use( express.static( __dirname + '/public' ));
  app.use( express.cookieParser());
  app.use( express.bodyParser());
  app.use( routes.current_user );
  app.use( app.router );
});

app.configure( 'development', function (){
  app.use( express.errorHandler({ dumpExceptions : true, showStack : true }));
});

app.configure( 'production', function (){
  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 );

app.listen( 3000, function (){
  console.log( 'Express server listening on port %d in %s mode', app.address().port, app.settings.env );
});
routes/index.js
var mongoose = require( 'mongoose' );
var Todo     = mongoose.model( 'Todo' );
var utils    = require( 'connect' ).utils;

exports.index = function ( req, res, next ){
  Todo.
    find({ user_id : req.cookies.user_id }).
    sort( '-updated_at' ).
    exec( function ( err, todos, count ){
      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 ){
    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 ){
  Todo.
    find({ user_id : req.cookies.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 ){
    if( todo.user_id !== req.cookies.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 ){
  if( !req.cookies.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