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 |