So I consider myself a Level 2 node.js/CoffeeScript beginner. I'm learning out in the open. Some of the things I come across are interesting from the perspective of someone climbing the learning curve. To gain experience, I wanted to share some things I've learned about one of the most basic functions of any web server: Serving static files.
Serving static files is something you might take for granted if you come from a background where the "website" is hosted on top of IIS or Apache. I never really gave it a lot of attention, but whatever framework I was using was silently, faithfully, serving up all my images, icons, style sheets, and scripts.
Node gives you pretty low-level access to the HTTP server. So when you make an HTTP application in Node -- you're technically writing the server. So this means you have to be responsible for handling all aspects of the web request and every web request including all those cool static files.
Writing it All from Scratch
So you might start the Node server up in the typical fashion.
http = require 'http' ... server = http.createServer serve_file port = 3000 server.listen port console.log "The static file server is on port #{port}"
The function
serve_file
is the callback that will handle the request.serve_file = (request, response) -> console.log "Serving #{request.url}" context = { request, response } fs.readFile 'public' + request.url, after_file_is_read(context)
The first thing the static file server has to do is actually read the file. We use the asynchronous version of the standard
readFile
method. I'm bundling up the request
and response
objects into something I'm calling context
. You'll see how I'm handling this object (notice I'm intentionally not using the word class) in subsequent method calls.after_file_is_read = (context) -> (error, data) -> console.log "The requested file has been read" context = context extends {error, data} respond_with_file(context) or respond_with_error(context)
The method
after_file_is_read
isn't the actual callback. It's a method that is invoked immediately passing the context and returns a function (the real callback) and captures the current context in a closure. This helps prevent the Christmas tree effect with nested anonymous callbacks. Essentially the sequence can be followed by looking at functions defined on the left side of the code.I'm probably being a little too clever (and maybe not in the good way), but something that is just too cool to pass up (during my experimentation phase) is to use the
extends
keyword to add the additional values of error
and data
into the "context". Essentially, extends
adds two additional properties (error
and data
) onto the exsisting object that already has a request
and response
properties. Now here's where it get's really clever. I call into the
respond_with_file
function. But what if there's an error? Shouldn't I be protecting the call to respond_with_file
with an if
statement and calling respond_with_error
in an else
block?respond_with_file = (context) -> if context.error then return undefined console.log "Responding with the file" {response, data} = context response.writeHead 200, {"Content-Type":"text/html"} response.end data @
I could, but instead, I'm turning the more conventional approach inside out. I'm making the absence of an error a pre-condition for executing the main body of the
respond_with_file
method. The function returns undefined
if the precondition is not met. This let's me compose the functions using the or
operator as I've done.Notice the "Content-Type" header. There's a bug (a.k.a. an unimplemented enhancement) here.
The
respond_with_error
function does what you might expect. Based on the error received from readFile
, it returns either a 404 or 500 response with the error message.Now start the application up and browse on over to http://localhost:3000.
Ugh error! Sorry no default handling for index.html. Ok, browse over to http://localhost:3000/index.html.
Success!! We're serving up a static HTML page!
With the source I included an image. Browse on over to http://localhost/bunny.png
Ummm...that doesn't look anything like a bunny. Remember the "Content-Type". Well, you have to deliver the right one based on the media type you're serving. Otherwise, the browser has every right to be confused.
Now the Easier, More Powerful, More Complete, More Extensible Way
Wouldn't it be nice if there was a library already built that let us do all that web server stuff? But it can't get in the way like some BAFx would. Well there is of course. ConnectJS.
All that code turns into this:
connect = require 'connect' server = connect() server.use connect.static('public') port = 3001 server.listen port console.log "Connect is listening at #{port}"
The primary difference being that now it's a much more complete and robust solution. Browse over to http://localhost:3001/bunny.png
It works by adding a concept Connect calls middleware. I think of middleware as a plugin. Essentially, all the middleware objects that are attached to the
connect()
object are run, and when one of them handles the request it stops. There is existing middleware for a lot of things including authentication, logging, and many many more. In the code above you can see we're attaching the static
middleware that serves up static content in the 'public' directory.What's more, middleware in the context of Connect isn't some heavyweight component technology. It's simply a function and a callback. Very elegant. Very powerful. Very simple. Writing a connect compliant middleware component is for another time (but it won't be a long post).
The source for these examples is on GitHub and the annotated source is the GitHub page.
What's Next
I got plenty of XP from this exercise, but not quite enough to level up. Still need to tackle:- Talking to a persistence store
- Handling authentication of users
- Logging
Two out of three are handled by standard Connect middleware.
Then I might be ready for level 3. Just enough to start fighting some of the bigger monsters, and getting into bigger trouble.