Express Web Framework has just recently become my framework of choice. I realize that Node seems like a bandwagon that everyone is jumping on, but consider this:
It can be a bit frustrating to get comfortable with Express mostly due to the lack of extensive documentation for it's API's, but once you get the hang of it, it's like finally mastering the Shoryuken. For those of you too young to remember Street Fighter 2, I apologize for that reference.
But as Levar Burton used to say, "You don't have to take my word for it". Strap on your scarf and skinny jeans and let's take a look at how Express really holds up under some pretty fundamental requirements.
You can grab the completed code for this project or view the demo.
Most apps start with a login/registration form. This sounds simple, but every time I build a form, I forget how much logic is involved. It's a good test for a web application framework since it involves request routing, form posting, session state, security, backend data operations and client/server data validation.
I am going to be using Everlive as my back-end data store for users. Telerik Everlive is a back-end service (sometimes referred to as a "Cloud") that offers data storage, user management, roles, email, push notifications and much much more. The best part is that developer accounts are completely free.
I'm just going to create a sample Everlive project called "Dashboard", and I'll think of something witty to set as the description.
Of course, NodeJS is a requirement for using Express as it's the platform on which express runs. Install Express (globally - means it's accessible from any place on your file system) using npm.
> npm install -g express
Then create the new "Dashboard" application.
> express dashboard
Express will then create a directory with that name and put the generated project files inside. It will ask you to navigate into that directory and install the dependencies when it's finished initializing the application.
> cd dashboard && npm install
Start the Express app with Node
> node app
You're up and running! Now it's time to get down to some real business.
Create login.jade
and register.jade
files. I'm not using the layout.jade
for these pages since I usually reserve that for the application layout.
Add the following code to the login.jade
page.
doctype 5 html head title Dashboard :: Login link(rel='stylesheet', href='//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css') link(rel='stylesheet', href='stylesheets/style.css') body .login.well h1 Please log in... hr form(id='login-form', method='POST', action='/login') .form-group label(for='username') Username input(type='text', name='username', class='form-control', id='username', placeholder='Enter username') .form-group label(for='password') Password input(type='password', name='password', class='form-control', id='password', placeholder='Enter password') .form-group button.btn.btn-default Login a(href="register") Or Register
Quick note on Jade: I was really perturbed by Jade the first time I used Express since it will not allow you to write HTML. You can use EJS templates for that, but Jade is really quite nice once you get a feel for it. It does take some getting used to, so give it a chance.
Now add the code for the register.jade
page.
doctype 5 html head title Dashboard :: Register link(rel='stylesheet', href='//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css') link(rel='stylesheet', href='stylesheets/style.css') body .login.well h1 Welcome aboard! hr form(id='register-form', method='POST', action='/register' novalidate) .form-group label(for='display') Display Name input(type='text', name='display', class='form-control', id='display', placeholder='Enter display name') .form-group label(for='username') Username input(type='text', name='username', class='form-control', id='username', placeholder='Enter username') .form-group label(for='password') Password input(type='password', name='password', class='form-control', id='password', placeholder='Enter password') .form-group label(for='confirm-password') Confirm Password input(type='password', name='confirm-password', class='form-control', id='confirm-password', placeholder='Confirm password') .form-group label(for='email') Email input(type='email', name='email', class='form-control', id='email', placeholder='Enter email') .form-group label(for='confirm-email') Confirm Email input(type='email', name='confirm-email', class='form-control', id='confirm-email', placeholder='Confirm email') .form-group button.btn.btn-default Register a(href="login") Or Login
Now we have login and register pages, but we have no way to get to them since we haven't defined routes in Express for them yet.
Open app.js
and add the following code in the routes section. I removed the user
route since I'm not going to use it.
app.get('/', routes.index); app.get('/login', routes.login); app.get('/register', routes.register);
Now open the routes/index.js
file so we can define these login
and register
methods. I'm going to redirect all root traffic to login as well since everyone needs to come through the front door. No cheating.
/* * GET home page. */ exports.index = function(req, res){ res.redirect('login'); }; /* * GET login page. */ exports.login = function(req, res) { res.render('login'); }; /* * GET register page. */ exports.register = function(req, res) { res.render('register'); };
Now visiting the application at http:localhost:3000
should take you to the login screen where you can navigate to the register page, and then back again.
Before we can log a user in, we have to be able to register one. Before we can register the user, we have to be able to validate that we have enough information to do so.
I am going to use Kendo UI to perform client-side validation on the registration form. Once I add it in, our registration page looks - well - more verbose.
doctype 5 html head title Dashboard :: Register link(rel='stylesheet', href='//cdn.kendostatic.com/2013.3.1119/styles/kendo.common.min.css') link(rel='stylesheet', href='//cdn.kendostatic.com/2013.3.1119/styles/kendo.bootstrap.min.css') link(rel='stylesheet', href='//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css') link(rel='stylesheet', href='stylesheets/style.css') body .login.well h1 Welcome aboard! hr form(id='register-form', method='POST', action='/register' novalidate) .form-group label(for='display') Display Name input(type='text', name='display', class='form-control', id='display', placeholder='Enter display name', required, data-required-msg='Display Name is required') .form-group label(for='username') Username input(type='text', name='username', class='form-control', id='username', placeholder='Enter username', required, data-required-msg='Username is required') .form-group label(for='password') Password input(type='password', name='password', class='form-control', id='password', placeholder='Enter password' required, data-required-msg='Password is required') .form-group label(for='confirm-password') Confirm Password input(type='password', name='confirm-password', class='form-control', id='confirm-password', placeholder='Confirm password' required, data-required-msg="Please confirm your password") .form-group label(for='email') Email input(type='email', name='email', class='form-control', id='email', placeholder='Enter email', required data-required-msg='Email address is required') .form-group label(for='confirm-email') Confirm Email input(type='email', name='confirm-email', class='form-control', id='confirm-email', placeholder='Confirm email', required data-required-msg='Please confirm your email address') .form-group button.btn.btn-default Register a(href="login") Or Login include messages script(src='//code.jquery.com/jquery-1.9.1.min.js') script(src='//cdn.kendostatic.com/2013.3.1119/js/kendo.all.min.js') script $(function() { // cache references to input controls var password = $('#password'); var confirmPassword = $('#confirm-password'); var email = $('#email'); var confirmEmail = $('#confirm-email'); $("#register-form").kendoValidator({ rules: { passwordMatch: function(input) { // passwords must match if (input.is('#confirm-password')) { return $.trim(password.val()) === $.trim(confirmPassword.val()); } return true; }, emailMatch: function(input) { // email addresses must match if (input.is('#confirm-email')) { return $.trim(email.val()) === $.trim(confirmEmail.val()); } return true; } }, messages: { // custom error messages. email gets picked up // automatically for any inputs of that type passwordMatch: 'The passwords don\'t match', emailMatch: 'The email addresses don\'t match', email: 'That doesn\'t appear to be a valid email address' } }).data('kendoValidator'); });
However, this form is rock solid in that Kendo UI will not allow the form to be submitted without all the proper values.
This forms posts to the register
URL, so add a route for that in app.js
.
// GET app.get('/', routes.index); app.get('/login', routes.login); app.get('/register', routes.register); // POST app.post('/register', routes.registerUser);
Now create the regiserUser
method in routes/index.js
. We can pull the form values right off of the request body. Everlive will want at least a username and password, but will take any additional fields like Email and DisplayName.
/* * POST register user. */ exports.registerUser = function(req, res) { // pull the form variables off the request body var username = req.body.username; var password = req.body.password; // additional registration information var additional = { Email: req.body.email, DisplayName: req.body.display }; // register the user...
WAIT. The client validated the input, but we can never fully rely on that. This is one of the truly maddening things about web development. We send our data to another place and time that we have no control over, so we can't be sure that what we get back has any integrity at all.
Node has a validator package that is exposed as "Express Middleware" called "Express Validator". Install it with NPM.
> npm install express-validator --save-dev
Then require it at the top of the app.js
file.
var express = require('express') , routes = require('./routes') , http = require('http') , path = require('path') , validator = require('express-validator');
"Middleware" simply means that we can use this library directly on the request and response HTTP objects in Express since this library is tied into the Express core.
Once the data has been run through the validator, we check for errors and then pass them back to the client if there are any. Just for testing, I'm going to pass back a success message that we'll eventually replace with the actual register functionality.
exports.registerUser = function(req, res) { // validate the input req.checkBody('username', 'Username is required').notEmpty(); req.checkBody('password', 'Password is required').notEmpty(); req.checkBody('display', 'DisplayName is required').notEmpty(); req.checkBody('email', 'Email is required').notEmpty(); req.checkBody('email', 'Email does not appear to be valid').isEmail(); // check the validation object for errors var errors = req.validationErrors(); console.log(errors); if (errors) { res.render('register', { flash: { type: 'alert-danger', messages: errors }}); } else { res.render('register', { flash: { type: 'alert-success', messages: [ { msg: 'No errors!' }]}}); } };
I then create a messages.jade
template which I can include in both the login and register pages which will display any messages we pass back from the server.
if flash div.alert(class='#{flash.type}') ul - for (var i = 0; i < flash.messages.length; i++) { li #{flash.messages[i].msg} - } ul
Then just include this template wherever you want the messages to show up in the login and registration pages.
include messages
We are finally ready to register users! I know it seems like a lot of code, but this is the reality of forms. This is why this is such a great exercise for testing and learning Express.
Everlive exposes it's API via REST URL, but there is also a JavaScript SDK that is exposed in the form of both a browser script and an npm package.
Install the Everlive SDK with NPM.
> npm install everlive-sdk --save-dev
In case you are wondering what --save-dev is for, we are using it to store our dependency on these new packages in the project
package.json
file. This way, you can have your mates pull the project down and all they have to do is the standardnpm install
when they set the project up. They will love and cherish you for this.
Grab your API Key from the Everlive project you created.
Include the Everlive SDK at the top of the routes/index.js
file and then initiliaze the library, passing in your API key. I also added a flash
variable at the top of the file that I can use to pass messages from other methods to a view.
var Everlive = require('everlive-sdk'); var key = "vtp....."; var el = new Everlive(key);
The Everlive SDK wraps all of the REST API configuration into some neat methods, and the best part is that they ALL return promises since they are asynchronous calls to a remote service.
Register the user calling the register
method on the Users
object. If the registration fails, set the flash message and return the register
method. If it's successful, return the login page and flash a message that they should confirm their account.
var Everlive = require('everlive-sdk'); var key = "vt........."; var flash = {}; var el = new Everlive(key); /* * GET home page. */ exports.index = function(req, res){ res.redirect('login'); }; /* * GET login page. */ exports.login = function(req, res) { res.render('login', { flash: flash }); }; /* * GET register page. */ exports.register = function(req, res) { res.render('register', { flash: flash }); }; /* * POST register user. */ exports.registerUser = function(req, res) { // validate the input req.checkBody('username', 'Username is required').notEmpty(); req.checkBody('password', 'Password is required').notEmpty(); req.checkBody('display', 'DisplayName is required').notEmpty(); req.checkBody('email', 'Email is required').notEmpty(); req.checkBody('email', 'Email does not appear to be valid').isEmail(); // check the validation object for errors var errors = req.validationErrors(); if (errors) { flash = { type: 'alert-danger', messages: errors }; res.redirect('register'); } else { // pull the form variables off the request body var username = req.body.username; var password = req.body.password; var additional = { Email: req.body.email, DisplayName: req.body.display }; // register the user with everlive el.Users.register(username, password, additional).then(function() { // success console.log('success'); flash.type = 'alert-success'; flash.messages = [{ msg: 'Please check your email to verify your registration. Then you will be ready to log in!' }]; res.render('login', { flash: flash }); }, function(error){ // failure console.log(error); flash.type = 'alert-danger'; flash.messages = [{ msg: error.message }]; res.render('register', { flash: flash }); }); } };
Now you can register a user with your Everlive back-end. If you fill out all the fields right, you are redirected to the login page and told to verify your email address.
Just for fun, go back and try to register with the same username again.
That's Everlive handling all of the account registration validation. Usernames are unique and so are email addresses.
Speaking of emails, Everlive should have just sent you an email when you registered your user. The email includes a link to verify your account. Don't click it just yet.
Everlive allows you to customize the email messages it sends, but not with the Developer Account
Let's implement the login functionality. Remember the login form? We created it at the very beginning of this article, and now we need it. Add the flash message template to the login.jade
code. We also need to validate it. Fortunately, that doesn't require nearly as much code as the registration page.
doctype 5 html head title Dashboard :: Login link(rel='stylesheet', href='//cdn.kendostatic.com/2013.3.1119/styles/kendo.common.min.css') link(rel='stylesheet', href='//cdn.kendostatic.com/2013.3.1119/styles/kendo.bootstrap.min.css') link(rel='stylesheet', href='//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css') link(rel='stylesheet', href='stylesheets/style.css') body .login.well h1 Please log in... hr form(id='login-form', method='POST', action='/login' novalidate) .form-group label(for='username') Username input(type='text', name='username', class='form-control', id='username', placeholder='Enter username', required, data-required-msg='Username is required') .form-group label(for='password') Password input(type='password', name='password', class='form-control', id='password', placeholder='Enter password', required, data-required-msg='Password is required') .form-group button.btn.btn-default Login a(href="register") Or Register include messages script(src='//code.jquery.com/jquery-1.9.1.min.js') script(src='//cdn.kendostatic.com/2013.3.1119/js/kendo.all.min.js') script $(function() { // validate the login form before submit $("#login-form").kendoValidator(); });
This form does an HTTP POST to the 'login' URL. By now, you should be getting the hang of what we have to do now.
Create the post method route in the app.js
file.
// GET app.get('/', routes.index); app.get('/register', routes.register); app.get('/login', routes.login); // POST app.post('/register', routes.registerUser); app.post('/login', routes.loginUser);
Now create the loginUser method in the routes/index.js
file which calls the login
method on the Everlive Users object.
exports.loginUser = function(req, res) { // pull the form variables off the request body var username = req.body.username; var password = req.body.password; // register the user with everlive el.Users.login(username, password).then(function(data) { // success res.render('dashboard') }, function(error){ // failure locals.type = 'error'; locals.message = error.message; res.render('login', { flash: flash }); }); };
The Dashboard view is the holy grail. It is the ENTIRE reason we have gone to all this trouble to register and login users. Go ahead and create it. I've added a simple Bootstrap navbar which will display the user's name and a logout button.
Right now, we just blindly log people in since the login
method for Everlive just returns a success or failure. Once we get a success, we can call the currentUser()
method to get information about the user. Then we can store it securely on the server for the life of the session using...
Sessions are secure data that is isolated to a specific user during a specific window of application use by way of cookies. Express includes session support in it's out-of-the-box Middleware package. We can add it in app.js
app.configure(function(){ app.set('port', process.env.PORT || 3000); app.set('views', __dirname + '/views'); app.set('view engine', 'jade'); app.use(express.favicon()); app.use(express.logger('dev')); app.use(validator([])); app.use(express.bodyParser()); app.use(express.methodOverride()); // add session support! app.use(express.cookieParser()); app.use(express.session({ secret: 'sauce' })); app.use(app.router); app.use(express.static(path.join(__dirname, 'public'))); });
Now modify the login method in routes/index.js
to check that the user verified their account and then store the user's information securely in the session.
exports.loginUser = function(req, res) { // pull the form variables off the request body var username = req.body.username; var password = req.body.password; // register the user with everlive el.Users.login(username, password).then(function(data) { // success req.session.authenticated = true; el.Users.currentUser().then(function(data) { // success // only log this user in if they have verified their account if (data.result.IsVerified) { req.session.user = data.result; res.render('dashboard'); } else { flash.type = 'alert-danger'; flash.messages = [{ msg: 'You have registered, but have not yet verified your account. Please check your email for registration confirmation and click on the provided link to verify your account.' }]; res.render('login', { flash: flash }); } }, function(error) { // failure flash.type = 'error'; flash.messages = [{ msg: error.message }]; res.render('login', { flash: flash }); }); }, function(error){ // failure flash.type = 'error'; flash.messages = [{ msg: error.message }]; res.render('login', { flash: flash }); }); };
We don't have to validate this time because Everlive isn't going to let us get very far without a username and password. Also, the user can't login until they have verified their account.
Once the user has verified their account, they can login and access the sacred Dashboard page. Actually, they can access it without logging in because we haven't restricted access to it yet.
Did I mention that Express is very minimalistic? We kind of need to implement our own function to enforce security. The good news is that it's incredibly easy since we can pass a second function to a route which intercepts the request and response and allows us to perform some function on them.
function restrict(req, res, next) { // if the user is authenticated... if (req.session.authenticated) { // PROCEEED next(); } else { // not authenticated - go to the back of the line res.redirect('/login'); } } // GET app.get('/', routes.index); app.get('/register', routes.register); app.get('/login', routes.login); // restrict the dashboard route app.get('/dashboard', restrict, routes.dashboard); // POST app.post('/register', routes.registerUser); app.post('/login', routes.loginUser);
Go ahead and try it out! I've hidden some great content for you behind this login page, but you're going to have to register to see it.
The full source code for this project is up on GitHub.
It is! It's a no-frills framework, and I think that's a GOOD thing. I like it. Having an intelligent backend system with a nice SDK really helps as well. So turn up that "Arcade Fire" album, put on those Buddy Holly glasses and embrace your inner hipster.
Burke Holland is a web developer living in Nashville, TN and was the Director of Developer Relations at Progress. He enjoys working with and meeting developers who are building mobile apps with jQuery / HTML5 and loves to hack on social API's. Burke worked for Progress as a Developer Advocate focusing on Kendo UI.