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.
**UPDATE**
The latest Express 4.x is a little different from 3.x used in the article. I updated the example code on Github. Please take a look and compare.
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
,update
anddelete
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.
- 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@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 isjade
, we are going to useejs
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 useUbuntu
MongoDB starts after boot, if you useMac
you will have to start it by the following command.
$ mongodCreate 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.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 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 :)