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 DemoFunctionalities
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,updateanddeletehis/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.
- How to setup a node.js development environment on Mac OSX Lion
- How to setup a node.js development environment on Ubuntu 11.04
- How to setup a node.js development environment on Windows
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 isjade, we are going to useejsin 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 useUbuntuMongoDB starts after boot, if you useMacyou will have to start it by the following command.
$ mongod --dbpath /usr/local/dbCreate 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.jsFirst 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 :)




