Learning Node.js

This post is part of my weekly tech learning series, where I take one hour each week to try out a piece of technology that I'd like to learn.

Since I've been learning JavaScript libraries the past few weeks (CoffeeScript, Knockout.js), I decided it was time jump over to the server and learn node.js.

Setup

Since I already had node.js installed I decided it would be best to use a virtual machine so I could use the latest version. Since I use vagrant, I already have Ubuntu configuration with Ruby 1.9.3, puppet, and various other tools. All that was left was to create a Vagrantfile, boot the server, and compile node.

### src/Vagrantfile

-*- mode: ruby -*-

vi: set ft=ruby :

require 'fileutils'

VM_NAME = 'node'

Vagrant::Config.run do |config| config.vm.customize ["modifyvm", :id, "--name", VM_NAME, "--memory", "512"] config.vm.box = "lucid64_with_ruby193_and_puppet"

config.vm.host_name = "#{VM_NAME}.home.theadmin.org" config.vm.forward_port 22, 2222, :auto => true config.vm.forward_port 80, 8080, :auto => true config.vm.forward_port 443, 8443, :auto => true config.vm.forward_port 8080, 8088, :auto => true config.vm.network :hostonly, "33.33.13.38" config.vm.share_folder "node", "/var/node", FileUtils.pwd end

Notice how this file is sharing the current working directory to /var/node. This lets me run my code on the vm, while editing it on my main computer.

The Todo App

After using the hello world example on node's homepage, I needed to think about how the todo app would work. For simplicity, I decided to just store the todo items in a flat file with some metadata.

Having the storage format decided, I then thought about what the public API would be. Since I've been doing a lot more client side JavaScript and API development, I decided to make the node server act like a RESTful JSON API server. This means it would follow many of the Rails conventions of:

Unfortunately, due to my hour time constraint I wasn't able to complete all of the routes.

I was able to complete the list, get one, add new, and a 404 catchall route. I also wasn't returning JSON back to the client, which was just an oversight.

Testing

Not wanting to dive in TDD for node, I still wanted a fast way of testing the application without refreshing my browser. The quick and dirty solution I came up with was a Ruby script that wrapped curl. This let me use a simple syntax on the command line to send requests.

./test.rb ./test.rb GET / ./test.rb GET /s (for 404 testing) ./test.rb GET /1 ./test.rb GET /2 ./test.rb POST / add.json (where add.json is a json file)

## test.rb #!/usr/bin/env ruby

@http_method = ARGV[0] || 'GET' @url = ARGV[1] || '/' @json_file = ARGV[2]

def request_url "http://0.0.0.0:8088#{@url}" end

curl_options = "-i "

if @json_file json = File.read(@json_file) curl_options += "-H 'Content-Type: application/json' -d '#{json}'" end

system("curl #{curl_options} -i -X #{@http_method} #{request_url}")

Screencast

Summary

Overall, node.js was okay. I was a bit surprised by how low level node was. From reading and hearing of node being a "Rails killer" I was expecting more.

Maybe it was a documentation problem as the official node.js site didn't have any tutorials other than a hello world. Or maybe node.js would be better compared to Sinatra where you have power but need to build things yourself. I don't know.

That said, if you look at node.js as a generic network server library then it works good. Layering a web frameworks on top of it like express.js might make it a good comparison to Rails. That will definitely be something I explore in later weeks.

(I also didn't evaluate node.js in production so I'm not comparing performance here either)

Code

This is the actual node.js server I was working on. In the screencast above I step through each of the major sections.

/// src/server.js var http = require('http'); var fs = require('fs'); var url = require('url');

var todoStore = "todo.txt";

function parseId(urlPath) { var id = url.parse(urlPath).pathname.match(/\/\d+/);

// Convert from '/123' to 123 (int) if (id) { id = id[0].replace('/',''); id = parseInt(id); }

return id; }

function render404(response) { response.writeHead(404, {'Content-Type': 'text/plain'}); response.end('Not found\n'); }

http.createServer(function (request, response) { // JSON input var data = '';

if (request.method === 'GET' && url.parse(request.url).pathname == '/') { /* GET / */

response.writeHead(200, {'Content-Type': 'text/plain'});
fs.readFile(todoStore, 'ascii', function(err, data) {
  response.end(data);
});

} else if (request.method === 'GET' && parseId(request.url)) { /* GET /:id */ // TODO: matches /1s11 as /1 var id = parseId(request.url);

response.writeHead(200, {'Content-Type': 'text/plain'});
fs.readFile(todoStore, 'ascii', function(err, data) {
  var dataAsLines = data.split("\\n");

  var todoItem = dataAsLines\[id - 1\];
  if (todoItem) {
    response.end(todoItem + "\\n");
  } else {
    render404(response);
  }
});

} else if (request.method === 'POST') { /* POST / */ request.addListener('data', function(chunk) { data += chunk; }); request.addListener('end', function() { var newTodo = JSON.parse(data); if (newTodo.status == null || newTodo.status == '') { newTodo.status = '-'; }

  var storeFormat = newTodo.status + " " + newTodo.content + "\\n";

  fs.appendFile(todoStore, storeFormat, 'ascii');

  response.writeHead(200, {'Content-Type': 'text/plain'});
  response.end('Added\\n');
});

} else { /* Remaining paths */ render404(response); }

}).listen(8080, '0.0.0.0');

console.log('Server running at http://0.0.0.0:8080')

/* File spec:

// TODO: PUT /:id

// TODO: DELETE /:id

Topics: Javascript Node js Tech learning

Would you like a daily tip about Shopify?

Each tip includes a way to improve your store: customer analysis, analytics, customer acquisition, CRO... plus plenty of puns and amazing alliterations.