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