óCoffeeScript Cookbook

Basic HTTP Server

Problem

You want to create a HTTP server over a network. Over the course of this recipe, we’ll go step by step from the smallest server possible to a functional key-value store.

Solution

We’ll use node.js’s HTTP library to our own selfish purposes and create the simplest web server possible in Coffeescript.

Say ‘hi\n’

We can start by importing node.js’s HTTP module. This contains createServer which, given a simple request handler, returns a HTTP server. We can use that server to listen on a TCP port.

http = require 'http'
server = http.createServer (req, res) -> res.end 'hi\n'
server.listen 8000

To run this example, simply put in a file and run it. You can kill it with Ctrl-C. We can test it using the curl command, available on most *nix platforms:

$ curl -D - http://localhost:8000/
HTTP/1.1 200 OK
Connection: keep-alive
Transfer-Encoding: chunked

hi

What’s going on?

Let’s get a little bit more feedback on what’s happening on our server. While we’re at it, we could also be friendlier to our clients and provide them some HTTP headers.

http = require 'http'

server = http.createServer (req, res) ->
    console.log req.method, req.url
    data = 'hi\n'
    res.writeHead 200,
        'Content-Type':     'text/plain'
        'Content-Length':   data.length
    res.end data

server.listen 8000

Try to access it once again, but this time use different URL paths, such as http://localhost:8000/coffee. You’ll see something like this on the server console:

$ coffee http-server.coffee 
GET /
GET /coffee
GET /user/1337

GETting stuff

What if our webserver was able to hold some data? We’ll try to come up with a simple key-value store in which elements are retrievable via GET requests. Provide a key on the request path and the server will return the corresponding value — or 404 if it doesn’t exist.

http = require 'http'

store = # we'll use a simple object as our store
    foo:    'bar'
    coffee: 'script'

server = http.createServer (req, res) ->
    console.log req.method, req.url
    
    value = store[req.url[1..]]

    if not value
        res.writeHead 404
    else
        res.writeHead 200,
            'Content-Type': 'text/plain'
            'Content-Length': value.length + 1
        res.write value + '\n'
    
    res.end()

server.listen 8000

We can try several URLs to see how it responds:

$ curl -D - http://localhost:8000/coffee
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 7
Connection: keep-alive

script

$ curl -D - http://localhost:8000/oops
HTTP/1.1 404 Not Found
Connection: keep-alive
Transfer-Encoding: chunked

Use your head(ers)

Let’s face it, text/plain is kind of lame. How about if we use something hip like application/json or text/xml? Also, our store retrieval process could use a bit of refactoring — how about some exception throwing & handling? Let’s see what we can come up with:

http = require 'http'

# known mime types
[any, json, xml] = ['*/*', 'application/json', 'text/xml']

# gets a value from the db in format [value, contentType]
get = (store, key, format) ->
    value = store[key]
    throw 'Unknown key' if not value
    switch format
        when any, json then [JSON.stringify({ key: key, value: value }), json]
        when xml then ["<key>#{ key }</key>\n<value>#{ value }</value>", xml]
        else throw 'Unknown format'

store =
    foo:    'bar'
    coffee: 'script'

server = http.createServer (req, res) ->
    console.log req.method, req.url
    
    try
        key = req.url[1..]
        [value, contentType] = get store, key, req.headers.accept
        code = 200
    catch error
        contentType = 'text/plain'
        value = error
        code = 404
 
    res.writeHead code,
        'Content-Type': contentType
        'Content-Length': value.length + 1
    res.write value + '\n'
    res.end()

server.listen 8000

This server will still return the value which matches a given key, or 404 if non-existent. But it will structure the response either in JSON or XML, according to the Accept header. See for yourself:

$ curl http://localhost:8000/
Unknown key

$ curl http://localhost:8000/coffee
{"key":"coffee","value":"script"}

$ curl -H "Accept: text/xml" http://localhost:8000/coffee
<key>coffee</key>
<value>script</value>

$ curl -H "Accept: image/png" http://localhost:8000/coffee
Unknown format

You gotta give to get back

The obvious last step in our adventure is to provide the client the ability to store data. We’ll keep our RESTiness by listening to POST requests for this purpose.

http = require 'http'

# known mime types
[any, json, xml] = ['*/*', 'application/json', 'text/xml']

# gets a value from the db in format [value, contentType]
get = (store, key, format) ->
    value = store[key]
    throw 'Unknown key' if not value
    switch format
        when any, json then [JSON.stringify({ key: key, value: value }), json]
        when xml then ["<key>#{ key }</key>\n<value>#{ value }</value>", xml]
        else throw 'Unknown format'

# puts a value in the db
put = (store, key, value) ->
    throw 'Invalid key' if not key or key is ''
    store[key] = value

store =
    foo:    'bar'
    coffee: 'script'

# helper function that responds to the client
respond = (res, code, contentType, data) ->
    res.writeHead code,
        'Content-Type': contentType
        'Content-Length': data.length
    res.write data
    res.end()

server = http.createServer (req, res) ->
    console.log req.method, req.url
    key = req.url[1..]
    contentType = 'text/plain'
    code = 404
    
    switch req.method
        when 'GET'
            try
                [value, contentType] = get store, key, req.headers.accept
                code = 200
            catch error
                value = error
            respond res, code, contentType, value + '\n'

        when 'POST'
            value = ''
            req.on 'data', (chunk) -> value += chunk
            req.on 'end', () ->
                try
                    put store, key, value
                    value = ''
                    code = 200
                catch error
                    value = error + '\n'
                respond res, code, contentType, value

server.listen 8000

Notice how the data is received in a POST request. By attaching some handlers on the 'data' and 'end' events of the request object, we’re able to buffer and finally save the data from the client in the store.

$ curl -D - http://localhost:8000/cookie
HTTP/1.1 404 Not Found # ...
Unknown key

$ curl -D - -d "monster" http://localhost:8000/cookie
HTTP/1.1 200 OK # ...

$ curl -D - http://localhost:8000/cookie
HTTP/1.1 200 OK # ...
{"key":"cookie","value":"monster"}

Discussion

Give http.createServer a function in the shape of (request, response) -> ... and it will return a server object, which we can use to listen on a port. Interact with the request and response objects to give the server its behaviour. Listen on port 8000 using server.listen 8000.

For API and overall information on this subject, check node.js’s http and https documentation pages. Also, the HTTP spec might come in handy.

Exercises

  • Create a layer in between the server and the developer which would allow the developer to do something like:
server = layer.createServer
    'GET /': (req, res) ->
        ...
    'GET /page': (req, res) ->
        ...
    'PUT /image': (req, res) ->
        ...