2 Todo App

Summary

Todo apps are considered quintessential in showcasing frameworks and are akin to the famous Todomvc.com for front-end JavaScript frameworks. In this example, we’ll use Jade, forms, LESS, AJAX/XHR and CSRF.

In our Todo app, we’ll intentionally not use Backbone.js or Angular to demonstrate how to build traditional websites with the use of forms and redirects. We’ll also explain how to plug in CSRF and LESS.

Example

All the source code is in github.com/azat-co/todo-express for your convenience.

Here are some screenshots of the Todo app in which we start from a home page:

The Todo app home page.

The Todo app home page.

There’s an empty list (unless you played with this app before):

The empty Todo List page.

The empty Todo List page.

Now we can add four items to the Todo List:

The Todo List page with added items.

The Todo List page with added items.

Mark one of the tasks as “done”, e.g.. “Buy milk”:

The Todo List page with one item marked done.

The Todo List page with one item marked done.

Going to the Complete page reveals this done item:

The Todo app Completed page.

The Todo app Completed page.

Deletion of an item from the Todo list is the only action performed via AJAX/XHR request. The rest of the logic is done via GETs and POSTs (by forms).

The Todo List page with removed tasks.

The Todo List page with removed tasks.

2.1 Scaffolding

As usual, we start by running

1 $ express todo-express
2 $ cd todo-express
3 $ npm install

This will give us the basic Express.js application.

We’ll need to add two extra dependencies to package.json, the less-middleware and Mongoskin libraries:

1 $ npm install less-middleware --save
2 $ npm install mongoskin --save

Changing the name to todo-express is optional:

 1 {
 2   "name": "todo-express",
 3   "version": "0.0.1",
 4   "private": true,
 5   "scripts": {
 6     "start": "node app.js"
 7   },
 8   "dependencies": {
 9     "express": "3.3.5",
10     "jade": "*",
11     "mongoskin": "~0.6.0",
12     "less-middleware": "~0.1.12"
13   }
14 }

2.2 MongoDB

Install MongoDB if you don’t have it already.

1 $ brew update
2 $ brew install mongodb
3 $ mongo --version

For more flavors of MongoDB installation, check out the official docs.

2.3 Structure

The final version of the app has the following folder/file structure (GitHub):

 1 /todo-express
 2   /public
 3     /bootstrap
 4       *.less
 5     /images
 6     /javascripts
 7       main.js
 8       jquery.js
 9     /stylesheets
10       style.css
11       main.less
12   /routes
13     tasks.js
14     index.js
15   /views
16     tasks_completed.jade
17     layout.jade
18     index.jade
19     tasks.jade
20   app.js
21   readme.md
22   package.json

The *.less in the bootstrap folder means there are a bunch of Twitter Bootstrap (the CSS framework) source files. They’re available at GitHub.

2.4 app.js

This is a breakdown of the Express.js-generated app.js file with the addition of routes, database, session, LESS and param middlewares.

Firstly, we import dependencies with the Node.js global require() function:

1 var express = require('express');

Similarly, we get access to our own modules, which are the app’s routes:

1 var routes = require('./routes');
2 var tasks = require('./routes/tasks');

The core http and path modules will be needed as well:

1 var http = require('http');
2 var path = require('path');

Mongoskin is a better alternative to the native MongoDB driver:

1 var mongoskin = require('mongoskin');

One line is all we need to get the database connection object. The first param follows the standard URI convention of protocol://username:password@host:port/database:

1 var db = mongoskin.db('mongodb://localhost:27017/todo?auto_reconnect', {safe:true\
2 });

The app itself:

1 var app = express();

In this middleware, we export the database object to all middlewares. By doing so, we’ll be able to perform database operations in the routes modules:

1 app.use(function(req, res, next) {
2   req.db = {};

We simply store the tasks collection in every request:

1   req.db.tasks = db.collection('tasks');
2   next();
3 })

This line allows us to access appname from within every Jade template:

1 app.locals.appname = 'Express.js Todo App'

We set the server port to either the environment variable, or if that’s undefined, to 3000:

1 app.set('port', process.env.PORT || 3000);

These statements tell Express.js where templates live and what file extension to prepend in case the extension is omitted during the render calls:

1 app.set('views', __dirname + '/views');
2 app.set('view engine', 'jade');

Display the Express.js favicon (the graphic in the URL address bar of browsers):

1 app.use(express.favicon());

The out-of-the-box logger will print requests in the terminal window:

1 app.use(express.logger('dev'));

The bodyParser() middleware is needed to painlessly access incoming data:

1 app.use(express.bodyParser());

The methodOverride() middleware is a workaround for HTTP methods that involve headers. It’s not essential for this example, but we’ll leave it here:

1 app.use(express.methodOverride());

To use CSRF, we need cookieParser() and session():

1 app.use(express.cookieParser());
2 app.use(express.session({
3   secret: '59B93087-78BC-4EB9-993A-A61FC844F6C9'
4 }));

The csrf() middleware itself. The order is important; in other words, csrf() must be preceded by cookieParser() and session():

1 app.use(express.csrf());

To process LESS stylesheets into CSS ones, we utilize less-middleware in this manner:

1 app.use(require('less-middleware')({
2   src: __dirname + '/public',
3   compress: true
4 }));

The other static files are also in the public folder:

1 app.use(express.static(path.join(__dirname, 'public')));

Remember CSRF? This is how we expose it to templates:

1 app.use(function(req, res, next) {
2   res.locals._csrf = req.session._csrf;
3   return next();
4 })

The router plug-in is enabled by this statement. It’s important to have this line after the less-middleware and csrf() lines above:

1 app.use(app.router);

It’s possible to configure different behavior based on environments:

1 if ('development' == app.get('env')) {
2   app.use(express.errorHandler());
3 }

When there’s a request that matches route/RegExp with :task_id in it, this block is executed:

1 app.param('task_id', function(req, res, next, taskId) {

The value of task ID is in taskId and we query the database to find that object:

1   req.db.tasks.findById(taskId, function(error, task){

It’s tremendously important to check for errors and empty results:

1     if (error) return next(error);
2     if (!task) return next(new Error('Task is not found.'));

If there’s data, store it in the request and proceed to the next middleware:

1     req.task = task;
2     return next();
3   });
4 });

Now it’s time to define our routes. We start with the home page:

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

The Todo List page:

1 app.get('/tasks', tasks.list);

This route will mark all tasks in the Todo List as completed if the user presses the all done button. In a RESP API, the HTTP method would be PUT but because we’re building classical web apps with forms, we have to use POST:

1 app.post('/tasks', tasks.markAllCompleted)

The same URL for adding new tasks is used for marking all tasks completed, but in the previous methods itself (markAllCompleted) you’ll see how we handle flow control:

1 app.post('/tasks', tasks.add);

To mark a single task completed, we use the aforementioned :task_id string in our URL pattern. (In REST API, this should have been a PUT request):

1 app.post('/tasks/:task_id', tasks.markCompleted);

Unlike with the POST route above, we utilize Express.js param middleware with a :task_id token:

1 app.del('/tasks/:task_id', tasks.del);

For our Completed page, we define this route:

1 app.get('/tasks/completed', tasks.completed);

In case of malicious attacks or mistyped URLs, it’s a user-friendly thing to catch all requests with *. Keep in mind that if we had a match previously, the Node.js won’t come to execute this block:

1 app.all('*', function(req, res){
2   res.send(404);
3 })

Finally, we spin up our application with the good ‘ol http method:

1 http.createServer(app).listen(app.get('port'),
2   function(){
3     console.log('Express server listening on port '
4       + app.get('port'));
5   }
6 );

The full content of the app.js file:

 1 /**
 2  * Module dependencies.
 3  */
 4 
 5 var express = require('express');
 6 var routes = require('./routes');
 7 var tasks = require('./routes/tasks');
 8 var http = require('http');
 9 var path = require('path');
10 var mongoskin = require('mongoskin');
11 var db = mongoskin.db('mongodb://localhost:27017/todo?auto_reconnect', {safe:true\
12 });
13 var app = express();
14 app.use(function(req, res, next) {
15   req.db = {};
16   req.db.tasks = db.collection('tasks');
17   next();
18 })
19 app.locals.appname = 'Express.js Todo App'
20 // all environments
21 
22 
23 app.set('port', process.env.PORT || 3000);
24 app.set('views', __dirname + '/views');
25 app.set('view engine', 'jade');
26 app.use(express.favicon());
27 app.use(express.logger('dev'));
28 app.use(express.bodyParser());
29 app.use(express.methodOverride());
30 app.use(express.cookieParser());
31 app.use(express.session({secret: '59B93087-78BC-4EB9-993A-A61FC844F6C9'}));
32 app.use(express.csrf());
33 
34 app.use(require('less-middleware')({ src: __dirname + '/public', compress: true }\
35 ));
36 app.use(express.static(path.join(__dirname, 'public')));
37 app.use(function(req, res, next) {
38   res.locals._csrf = req.session._csrf;
39   return next();
40 })
41 app.use(app.router);
42 
43 // development only
44 if ('development' == app.get('env')) {
45   app.use(express.errorHandler());
46 }
47 app.param('task_id', function(req, res, next, taskId) {
48   req.db.tasks.findById(taskId, function(error, task){
49     if (error) return next(error);
50     if (!task) return next(new Error('Task is not found.'));
51     req.task = task;
52     return next();
53   });
54 });
55 
56 app.get('/', routes.index);
57 app.get('/tasks', tasks.list);
58 app.post('/tasks', tasks.markAllCompleted)
59 app.post('/tasks', tasks.add);
60 app.post('/tasks/:task_id', tasks.markCompleted);
61 app.del('/tasks/:task_id', tasks.del);
62 app.get('/tasks/completed', tasks.completed);
63 
64 app.all('*', function(req, res){
65   res.send(404);
66 })
67 http.createServer(app).listen(app.get('port'), function(){
68   console.log('Express server listening on port ' + app.get('port'));
69 });

2.5 Routes

There are only two files in the routes folder. One of them serves the home page (e.g., http://localhost:3000/) and is straightforward:

1 /*
2  * GET home page.
3  */
4 
5 exports.index = function(req, res){
6   res.render('index', { title: 'Express.js Todo App' });
7 };

The remaining logic that deals with tasks itself has been placed in todo-express/routes/tasks.js. Let’s break it down a bit.

We start by exporting a list() request handler that gives us a list of incomplete tasks:

1 exports.list = function(req, res, next){

To do so, we perform a database search with completed=false query:

1   req.db.tasks.find({
2     completed: false
3   }).toArray(function(error, tasks){

In the callback, we need to check for any errors: javascript if (error) return next(error);

Since we use toArray(), we can send the date directly to the template:

1     res.render('tasks', {
2       title: 'Todo List',
3       tasks: tasks || []
4     });
5   });
6 };

Adding a new task requires us to check for the name parameter:

1 exports.add = function(req, res, next){
2   if (!req.body || !req.body.name)
3     return next(new Error('No data provided.'));

Thanks to our middleware, we already have a database collection in the req object, and the default value for the task is incomplete (completed: false):

1   req.db.tasks.save({
2     name: req.body.name,
3     completed: false
4   }, function(error, task){

Again, it’s important to check for errors and propagate them with the Express.js next() function:

1     if (error) return next(error);
2     if (!task) return next(new Error('Failed to save.'));

Logging is optional; however, it’s useful for learning and debugging:

1     console.info('Added %s with id=%s', task.name, task._id);

Lastly, we redirect back to the Todo List page when the saving operation has finished successfully:

1     res.redirect('/tasks');
2   })
3 };

This method marks all incomplete tasks as complete:

1 exports.markAllCompleted = function(req, res, next) {

Because we had to reuse the POST route and since it’s a good illustration of flow control, we check for the all_done parameter to decide if this request comes from the all done button or the add button:

1   if (!req.body.all_done
2     || req.body.all_done !== 'true')
3     return next();

If the execution has come this far, we perform a db query with multi: true: javascript req.db.tasks.update({ completed: false }, {$set: { completed: true }}, {multi: true}, function(error, count){

Significant error handling, logging and redirection back to Todo List page:

1     if (error) return next(error);
2     console.info('Marked %s task(s) completed.', count);
3     res.redirect('/tasks');
4   })
5 };

The Completed route is similar to the Todo List, except for the completed flag value (true in this case):

 1 exports.completed = function(req, res, next) {
 2   req.db.tasks.find({
 3     completed: true
 4   }).toArray(function(error, tasks) {
 5     res.render('tasks_completed', {
 6       title: 'Completed',
 7       tasks: tasks || []
 8     });
 9   });
10 };

This is the route that takes care of marking a single task as done. We use updateById but the same thing can be accomplished with a plain update method from Mongoskin/MongoDB API. The trick with completed: req.body.completed === 'true is needed because the incoming value is a string and not a boolean.

1 exports.markCompleted = function(req, res, next) {
2   if (!req.body.completed)
3     return next(new Error('Param is missing'));
4   req.db.tasks.updateById(req.task._id, {
5     $set: {completed: req.body.completed === 'true'}},
6     function(error, count) {

Once more, we perform error and results checks; (update() and updateById() don’t return object, but the count of the affected documents instead):

 1       if (error) return next(error);
 2       if (count !==1)
 3         return next(new Error('Something went wrong.'));
 4       console.info('Marked task %s with id=%s completed.',
 5         req.task.name,
 6         req.task._id);
 7       res.redirect('/tasks');
 8     }
 9   )
10 }

Delete is the single route called by an AJAX request. However, there’s nothing special about its implementation. The only difference is that we don’t redirect, but send status 200 back.

Just for your information, the remove() method can be used instead of removeById().

 1 exports.del = function(req, res, next) {
 2   req.db.tasks.removeById(req.task._id, function(error, count) {
 3     if (error) return next(error);
 4     if (count !==1) return next(new Error('Something went wrong.'));
 5     console.info('Deleted task %s with id=%s completed.',
 6       req.task.name,
 7       req.task._id);
 8     res.send(200);
 9   });
10 }

For your convenience, here’s the full content of the todo-express/routes/tasks.js file:

 1 /*
 2  * GET users listing.
 3  */
 4 
 5 exports.list = function(req, res, next){
 6   req.db.tasks.find({completed: false}).toArray(function(error, tasks){
 7     if (error) return next(error);
 8     res.render('tasks', {
 9       title: 'Todo List',
10       tasks: tasks || []
11     });
12   });
13 };
14 
15 exports.add = function(req, res, next){
16   if (!req.body || !req.body.name) return next(new Error('No data provided.'));
17   req.db.tasks.save({
18     name: req.body.name,
19     completed: false
20   }, function(error, task){
21     if (error) return next(error);
22     if (!task) return next(new Error('Failed to save.'));
23     console.info('Added %s with id=%s', task.name, task._id);
24     res.redirect('/tasks');
25   })
26 };
27 
28 exports.markAllCompleted = function(req, res, next) {
29   if (!req.body.all_done || req.body.all_done !== 'true') return next();
30   req.db.tasks.update({
31     completed: false
32   }, {$set: {
33     completed: true
34   }}, {multi: true}, function(error, count){
35     if (error) return next(error);
36     console.info('Marked %s task(s) completed.', count);
37     res.redirect('/tasks');
38   })
39 };
40 
41 exports.completed = function(req, res, next) {
42   req.db.tasks.find({completed: true}).toArray(function(error, tasks) {
43     res.render('tasks_completed', {
44       title: 'Completed',
45       tasks: tasks || []
46     });
47   });
48 };
49 
50 exports.markCompleted = function(req, res, next) {
51   if (!req.body.completed) return next(new Error('Param is missing'));
52   req.db.tasks.updateById(req.task._id, {$set: {completed: req.body.completed ===\
53  'true'}}, function(error, count) {
54     if (error) return next(error);
55     if (count !==1) return next(new Error('Something went wrong.'));
56     console.info('Marked task %s with id=%s completed.', req.task.name, req.task.\
57 _id);
58     res.redirect('/tasks');
59   })
60 };
61 
62 exports.del = function(req, res, next) {
63   req.db.tasks.removeById(req.task._id, function(error, count) {
64     if (error) return next(error);
65     if (count !==1) return next(new Error('Something went wrong.'));
66     console.info('Deleted task %s with id=%s completed.', req.task.name, req.task\
67 ._id);
68     res.send(200);
69   });
70 };

2.6 Jades

In the Todo app, we use four templates:

  • layout.jade: the skeleton of HTML pages that is used on all pages
  • index.jade: home page
  • tasks.jade: Todo List page
  • tasks_completed.jade: Completed page

Let’s go through each file starting with layout.jade. It starts with doctype, html and head types:

1 doctype 5
2 html
3   head

We should have the appname variable set:

1     title= title + ' | ' + appname

Next, we include *.css files but underneath, and Express.js will serve its contents from LESS files:

1     link(rel="stylesheet", href="/stylesheets/style.css")
2     link(rel="stylesheet", href="/bootstrap/bootstrap.css")
3     link(rel="stylesheet", href="/stylesheets/main.css")

The body with Twitter Bootstrap structure consist of .container and .navbar. To read more about those and other classes, go to getbootstrap.com/css/:

 1   body
 2     .container
 3       .navbar.navbar-default
 4         .container
 5           .navbar-header
 6             a.navbar-brand(href='/')= appname
 7       .alert.alert-dismissable
 8       h1= title
 9       p Welcome to Express.js Todo app by 
10         a(href='http://twitter.com/azat_co') @azat_co
11         |. Please enjoy.

This is the place where other jades (like tasks.jade) will be imported:

1       block content

The last lines include front-end JavaScript files:

1   script(src='/javascripts/jquery.js', type="text/javascript")
2   script(src='/javascripts/main.js', type="text/javascript")

The full layout.jade file:

 1 doctype html
 2 html
 3   head
 4     title= title + ' | ' + appname
 5     link(rel="stylesheet", href="/stylesheets/style.css")
 6     link(rel="stylesheet", href="/bootstrap/bootstrap.css")
 7     link(rel="stylesheet", href="/stylesheets/main.css")
 8 
 9   body
10     .container
11       .navbar.navbar-default
12         .container
13           .navbar-header
14             a.navbar-brand(href='/')= appname
15       .alert.alert-dismissable
16       h1= title
17       p Welcome to Express.js Todo app by 
18         a(href='http://twitter.com/azat_co') @azat_co
19         |. Please enjoy.
20       block content
21   script(src='/javascripts/jquery.js', type="text/javascript")
22   script(src='/javascripts/main.js', type="text/javascript")

The index.jade file is our home page and it’s quite vanilla. The most interesting thing it has is the nav-pills menu:

 1 extends layout
 2 
 3 block content
 4   .menu
 5     h2 Menu
 6     ul.nav.nav-pills
 7       li.active
 8         a(href="/tasks") Home
 9       li
10         a(href="/tasks") Todo List
11       li
12         a(href="/tasks") Completed
13   .home
14     p This is an example of classical (no front-end JavaScript frameworks) web ap\
15 plication built with Express.js 3.3.5 for 
16       a(href="http://expressjsguide.com") Express.js Guide
17       |.
18     p The full source code is available at 
19       a(href='http://github.com/azat-co/todo-express') github.com/azat-co/todo-ex\
20 press
21       |.

The tasks.jade uses extends layout:

1 extends layout
2 
3 block content

Then goes our main page of specific content:

 1   .menu
 2     h2 Menu
 3     ul.nav.nav-pills
 4       li
 5         a(href='/') Home
 6       li.active
 7         a(href='/tasks') Todo List
 8       li
 9         a(href="/tasks/completed") Completed
10   h1= title

The div with list class will hold the Todo List:

1   .list
2     .item.add-task

The form to mark all items as done has a CSRF token in a hidden field and uses the POST method pointed to /tasks:

1       div.action
2         form(action='/tasks', method='post')
3           input(type='hidden', value='true', name='all_done')
4           input(type='hidden', value=locals._csrf, name='_csrf')
5           input(type='submit', class='btn btn-success btn-xs', value='all done')

A similar CSRF-enabled form is used for new task creation:

1       form(action="/tasks", method='post')
2         input(type='hidden', value=locals._csrf, name='_csrf')
3         div.name
4           input(type="text", name="name", placeholder='Add a new task')
5         div.delete
6           input.btn.btn-primary.btn-sm(type="submit", value='add')

When we start the app for the first time (or clean the database), there are no tasks:

1     if (tasks.length === 0)
2       | No tasks.

Jade supports iterations with the each command:

1     each task, index in tasks
2       .item
3         div.action

This form submits data to its individual task route:

1           form(action='/tasks/#{task._id}', method='post')
2             input(type='hidden', value=task._id.toString(), name='id')
3             input(type='hidden', value='true', name='completed')
4             input(type='hidden', value=locals._csrf, name='_csrf')
5             input(type='submit', class='btn btn-success btn-xs task-done', value=\
6 'done')

The index variable is used to display order in the list of tasks:

1         div.num
2           span=index+1
3             |. 
4         div.name
5           span.name=task.name
6           //- no support for DELETE method in forms
7           //- http://amundsen.com/examples/put-delete-forms/
8           //- so do XHR request instead from public/javascripts/main.js

The delete button doesn’t have anything fancy attached to it, because events are attached to these buttons from the main.js front-end JavaScript file:

1         div.delete
2           a(class='btn btn-danger btn-xs task-delete', data-task-id=task._id.toSt\
3 ring(), data-csrf=locals._csrf) delete

The full source code of tasks.jade:

 1 extends layout
 2 
 3 block content
 4 
 5   .menu
 6     h2 Menu
 7     ul.nav.nav-pills
 8       li
 9         a(href='/') Home
10       li.active
11         a(href='/tasks') Todo List
12       li
13         a(href="/tasks/completed") Completed
14   h1= title
15 
16   .list
17     .item.add-task
18       div.action
19         form(action='/tasks', method='post')
20           input(type='hidden', value='true', name='all_done')
21           input(type='hidden', value=locals._csrf, name='_csrf')
22           input(type='submit', class='btn btn-success btn-xs', value='all done')
23       form(action="/tasks", method='post')
24         input(type='hidden', value=locals._csrf, name='_csrf')
25         div.name
26           input(type="text", name="name", placeholder='Add a new task')
27         div.delete
28           input.btn.btn-primary.btn-sm(type="submit", value='add')
29     if (tasks.length === 0)
30       | No tasks.
31     each task, index in tasks
32       .item
33         div.action
34           form(action='/tasks/#{task._id}', method='post')
35             input(type='hidden', value=task._id.toString(), name='id')
36             input(type='hidden', value='true', name='completed')
37             input(type='hidden', value=locals._csrf, name='_csrf')
38             input(type='submit', class='btn btn-success btn-xs task-done', value=\
39 'done')
40         div.num
41           span=index+1
42             |. 
43         div.name
44           span.name=task.name
45           //- no support for DELETE method in forms
46           //- http://amundsen.com/examples/put-delete-forms/
47           //- so do XHR request instead from public/javascripts/main.js
48         div.delete
49           a(class='btn btn-danger btn-xs task-delete', data-task-id=task._id.toSt\
50 ring(), data-csrf=locals._csrf) delete

Last but not least comes tasks_completed.jade, which is just a stripped down version of the tasks.jade file:

 1 extends layout
 2 
 3 block content
 4 
 5   .menu
 6     h2 Menu
 7     ul.nav.nav-pills
 8       li
 9         a(href='/') Home
10       li
11         a(href='/tasks') Todo List
12       li.active
13         a(href="/tasks/completed") Completed
14 
15   h1= title
16 
17   .list
18     if (tasks.length === 0)
19       | No tasks.
20     each task, index in tasks
21       .item
22         div.num
23           span=index+1
24             |. 
25         div.name.completed-task
26           span.name=task.name

2.7 LESS

As we’ve mentioned before, after applying proper middleware in app.js files, we can put *.less files anywhere under the public folder. Express.js works by accepting a request for some .css file and tries to match the corresponding file by name. Therefore, we include *.css files in our jades.

Here is the content of the todo-express/public/stylesheets/main.less file:

 1 * {
 2   font-size:20px;
 3 }
 4 .btn {
 5   // margin-left: 20px;
 6   // margin-right: 20px;
 7 }
 8 .num {
 9   // margin-right: 3px;
10 }
11 .item {
12   height: 44px;
13   width: 100%;
14   clear: both;
15   .name {
16     width: 300px;
17   }
18   .action {
19     width: 100px;
20   }
21   .delete {
22     width: 100px
23   }
24   div {
25     float:left;
26   }
27 }
28 .home {
29   margin-top: 40px;
30 }
31 .name.completed-task {
32   text-decoration: line-through;
33 }

2.8 Conclusion

The Todo app is considered classic because it doesn’t rely on any front-end framework. This was done intentionally to show how easy it is to use Express.js for such tasks. In modern-day development, people often leverage some sort of REST API server architecture with a front-end client built with Backbone.js, Angular, Ember or something else. Our next examples dive into the details about how to write such servers.