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.

  • Identifiers based on line number.
  • First character is used for the status.
    • – for active items
    • X for complete items
    • D for deleted items
  • Second character is space for visual separation.
  • Todo item is string
  • \n are replaced by NEWLINE
  • Deleting an item is similar to an update but setting status to D

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:

  • GET / to list the todo items
  • GET /:id to return the specific todo item
  • POST / to create a new todo
  • PUT /:id to update an existing todo
  • DELETE /:id to delete a todo (though this would actually just soft-delete it in the file)

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:
- Identifiers based on line number
- First character is used for the status.
-- - for active items
-- X for complete items
-- D for deleted items
- Second character is space (for visual separation)
- Todo item is string
- \n are replaced by NEWLINE
- Deleting an item is similar to an update but setting status to D
*/
 
// TODO: PUT /:id
 
// TODO: DELETE /:id