Writing Node apps and bleeding everywhere
Well over two years ago, my wife and I renamed our side-business from Monkey Assassin Design Co to Jordan Rift. Along with the renaming, of course, we needed to rebrand everything. That meant new logo, new website, new business cards, etc.
Of course, I was a HUGE slacker when it came to all of these tasks. I've gone through 5 or 6 different iterations of the logo over the past two years. Many of them have been shot down by my better half (she was always right :). Some of those comps I've actually posted here previously. I'd often use this as an excuse for not having a website complete. For each iteration of the logo, I would convince myself that I had to completely redesign the website.
I actually had a few solid designs complete for the most part and nearly ready to go, but I never pulled the trigger. I'm really not sure why, but in hindsight I'm a little glad I didn't. Even though it cost us some additional time, I'm much happier with the final result than I have been with any of my other attempts.
If you've been hanging around me in the past few months, you know I'm a little nuts over JavaScript. So naturally, I wanted my business website to be an extension of that. I decided to write it in JavaScript, front to back. Here's my stack:
Node.JS on the server side
Express driving the HTTP functionality
Socket.IO driving much of the client/server interaction
Redis handling session management
All lovingly written in CoffeeScript
Client-side code all written using Backbone
Hosted on my beta account via the great guys over at Nodejitsu
So without further ado, check out my latest creation. I'm pretty happy with the way it came out. And most importantly, Mrs Offutt is happy with it too.
Now this wouldn't be a very valuable post if I just displayed my site for everybody to gush over. I'm working to make an effort in my posts to present some value. Hopefully others can benefit from my fumbling around. So here goes nothing.
// Insert something helpful below...
I designed the new JordanRift.com as a single page app. It uses Backbone and HTML5 PushState to manage navigating between different views and updating the browser's history, while showing off some pretty scrolling transitions.
This presented an interesting challenge. PushState allows you to do client navigation without refreshing the page or resorting to hash-based navigation. The challenge this presents is that in order to make urls bookmarkable, you have to mirror routing on both the client and the server. Let's assume we have a Backbone routing structure that looks like this:
// Backbone code running on the client... var FooRouter = Backbone.Router.extend({ routes: { '': 'index', 'foo': 'foo', 'bar': 'bar' }, index: function() { // handle default route }, foo: function() { // handle foo route }, bar: function() { // handle bar route } }); $(function() { var router = new FooRouter(); Backbone.history.start({ pushState: true }); });
Because we're exposing 3 URL routes on the client, we'll need to have those reflected on the server. Because somebody could hit your page from an external link (e.g. - 'http://yoursite.com/foo'), routes on the server will be needed so a 404 doesn't get served up and leave your viewers staring at an error page.
// Node.js code running on the server... var express = require('express'), app = module.exports = express.createServer(); // ... configuration stuff... app.get('/', function(req, res) { req.render('index'); }); app.get('/foo', function(req, res) { req.render('index'); }); app.get('/bar', function(req, res) { req.render('index'); });
There may be a better way of doing that, but I just ended up creating separate route handlers in Express for each URL that would be exposed on via Backbone. Of course they're not quite that simple in production, but that's the gist of it.
The other interesting challenge came in implementing my socket server. Socket.IO needs access to a session store. By default it will use the "InMemory" session store. If you're using Connect (if you're using Express, you're already using Connect), you can actually have the two handshake and share session stores. This helps tie things together quite nicely.
The default InMemory store works great if you're only using a single Node instance. But if you ever plan on running your app in production, you'll need something more robust, because you're likely going to be hosted on more than one instance of Node. This is where Redis comes in. You'll often hear, if you work in the tech industry, that when you're using a new technology, there will be some bleeding involved. That proved very true in my first experience with Redis. I bled all over the place, but it was highly educational.
When browsing through the node-redis documentation, you will see examples that look like this:
var redis = require('redis'), client = redis.createClient(); client.get('somekey', function(err, data) { if (!err) { // do something with 'data' } });
What the documentation doesn't tell you is that when you provision a new Redis database through your hosting provider (assuming you're not using a dedicated server), is that while it creates the Redis instance for you, it just provides you a URL that looks something like this:
Now I may be many things, but I'm no *nix genius, so this looked pretty cryptic to me. Even the documentation was lacking a bit in this case. The node-redis createClient method takes a port and host argument, so my first guess was to try something like this:
var redis = require('redis'), client = redis.createClient(9117, 'redis://nodejitsu:[email protected]');
I was getting connection errors, and it required a bit of head scratching and research to figure out. This is actually what needs to happen:
var redis = require('redis'), client = redis.createClient(9117, 'cod.redistogo.com');
So that's great, but the connection errors were still happening. That was less apparent to me what needed to happen, but after more research, I figured out that the URL returned by Nodejitsu actually used a common pattern:
OK, so now I needed to figure out how to authenticate against my Redis database. Unfortunately the createClient method does not accept a 'password' parameter. It does, however, have an 'auth' method. Here's the final result:
var redis = require('redis'), client = redis.createClient(9117, 'cod.redistogo.com'); // Magical 'auth' method of auth-omeness client.auth('SOME-LONG-HASH');
So there we have it. My fancy new app was working with Redis. Socket.IO and Connect were handshaking and sharing my shiny new Redis session/cache store. Here's the resulting code:
var connect = require('connect'), express = require('express'), redis = require('redis'), RedisStore = require('node-redis')(express), client = redis.creatClient(9117, 'cod.redistogo.com'), app = module.exports = express.createServer(), parseCookie = require('connect').utils.parseCookie, sessionStore, port, io; redis.auth('SOME-PASSWORD-HASH'); sessionStore = new RedisStore(client); app.set('views', __dirname + '/views'); app.set('view engine', 'jade'); app.use(connect.cookieParser()); app.use(connect.bodyParser()); app.use(connect.session({ secret: 'keyboard cat', store: sessionStore })); app.use(express.static(__dirname + '/public')); app.get('/', function(req, res) { req.render('index'); }); app.get('/foo', function(req, res) { req.render('index'); }); app.get('/bar', function(req, res) { req.render('index'); }); port = process.env.port || 3000; app.listen(port, function() { console.log('App is listening on ' + port + '.'); }); io = require('socket.io').listen(app); // Here's the handshake magic between Socket.IO and Connect io.configure(function() { io.set('authorization', function(data, callback) { var sessionID; if (data.headers.cookie) { data.cookie = parseCookie(data.headers.cookie); sessionID = data.cookie['connect.sid']; sessionStore.get(sessionID, function(err, session) { if (err || !session) { callback(new Error('no session found')); } else { callback(null, true); } }); } else { callback(new Error('no cookie sent')); } }); }); // Now within Socket.IO transmission, I can fetch session information... io.sockets.on('connection', function(socket) { // Here's my session ID... var sessionID = socket.handshake.cookie['connect.sid']; // ... And you can do stuff with it... socket.on('foo', function(data) { var key = 'foo:' + sessionID; client.get(key, function(err, result) { if (!err) { // Do something spiffy with 'result' now... } }); }); });
It's not easiest transition in the world coming from an ASP.NET background where the runtime takes care of all this stuff for you, but being exposed to it and having a healthy understanding of what's involved can be very beneficial.
So that's what I've got for now. Hopefully, if you're in the same boat I was in, this will help you on your way. If you have any questions, or if I botched something, please let me know in the comments.