by Matt Brennan

Building a simple REST API with Livewire

This post is adapted from Modulus’ excellent introduction to HAPI.

I might have mentioned Livewire around here a couple of times. Livewire is my little (and getting littler) HTTP router with a functional bent, for Node.js. Since 0.5, released this weekend, it’s been based on Highland, a high-level streams library.

Getting started

You’ll need Node.js 0.10, which comes bundled with npm. Create a package.json, which you can do by hand or with npm init. We’ll install a few things:

npm install --save livewire oban dram

A simple server

We’ll require Oban and http and create a server:

var handle = require('oban');
var http   = require('http');

var server = http.createServer(handle());
server.listen(process.env.PORT || 3000);

This doesn’t actually do anything on its own. Add a function to handle the request, and send a response, using a Livewire router:

var handle = require('oban');
var http   = require('http');
var route  = require('livewire');
var resp   = require('dram');

var server = http.createServer(handle(
	route.get('/', function(req) {
		return resp.ok('dreams came on in the Japanese night');
	})
));
server.listen(process.env.PORT || 3000);

That’s the complete server. Notice that it’s all code, not configuration. Start it up, and curl localhost:3000:

$ curl -i localhost:3000

HTTP/1.1 200 OK
Date: Tue, 15 Jul 2014 21:17:47 GMT
Connection: keep-alive
Transfer-Encoding: chunked

dreams came on in the Japanese night

Basic REST API

As per the HAPI post, we’ll build a simple API to retrieve and save quotes. Let’s start with a list of quotes, and a route to get them all.

var handle = require('oban');
var http   = require('http');
var route  = require('livewire');
var resp   = require('dram');

var quotes = [{
  author: 'Audrey Hepburn',
  text: 'Nothing is impossible, the word itself says \'I\'m possible\'!'
}, {
  author: 'Walt Disney',
  text: 'You may not realize it when it happens, but a kick in the teeth may be the best thing in the world for you'
}, {
  author: 'Unknown',
  text: 'Even the greatest was once a beginner. Don\'t be afraid to take that first step.'
}, {
  author: 'Neale Donald Walsch',
  text: 'You are afraid to die, and you\'re afraid to live. What a way to exist.'
}];

function json(obj) {
  return resp.ok(JSON.stringify(obj))
    .withHeader('Content-Type', 'application/json');
}

http.createServer(handle(
  route.get('/quotes', function() {
    return json(quotes);
  })
)).listen(process.env.PORT || 3000);

Unlike HAPI, Livewire doesn’t guess content types or serialise objects, so we’re doing it manually. Dram’s chaining API lets us do this quite nicely; I’ve pulled out a function to format the responses, as we’ll be using it quite a bit.

Now, we’ll add a /random route, which will return a random quote. Up till now, we’ve been using a single route at a time; for multiple routes, Livewire has the route function, which takes an array of handler functions, and returns the first matching result. Modify your server like so:

http.createServer(handle(route.route([
  route.get('/quotes', function() {
    return json(quotes);
  }),
  route.get('/random', function() {
  	var id = Math.floor(Math.random() * quotes.length);
  	return json(quotes[id]);
  })
])))

While we’re here, we can add a fallback route, to handle any path the other routes can’t. Since a route is just a function returning a stream, we can add a function always returning a 404 response at the end of our routes array.

http.createServer(handle(route.route([
  route.get('/quotes', function() {
    return json(quotes);
  }),
  route.get('/random', function() {
  	var id = Math.floor(Math.random() * quotes.length);
  	return json(quotes[id]);
  }),
  function(req) { return resp.notFound(req.url + ' not found'); }
])))

Up next is returning a single quote by its index. We add a URL parameter to a route; this is an identifier preceded by a colon, such as /quote/:id.

NB from here on in, I’ll just be giving the routes; these should be added to the array, before the fallback.

route.get('/quote/:id', function(req) {
  if(quotes[req.params.id]) {
  	return json(quotes[req.params.id]);
  }
  return resp.notFound('No quote found');
})

Now, we’ll add a POST route to add a quote to the array. I won’t be covering validation, mostly because there’s no Oban-compatible module to do it yet.

route.post('/quote', function(req) {
  return route.json(req).flatMap(function(body) {
  	var newquote = {
  	  author: body.author,
  	  text: body.text
  	};
  	quotes.push(newquote);
  	return json(newquote);
  });
})

This nicely highlights Livewire’s way of dealing with asynchronous responses. Since reading the request body stream is asynchronous, we have a streaming body parser, which decodes the body and sends it as a stream containing a single object. flatMap is just to transform the object stream into a response stream: notice we’re still doing return json(...), even within the asynchronous call. curl a POST request, and we get the new quote back:

$ curl -X POST -i localhost:3000/quote -d '{"author":"Matt Brennan", "text":"Livewire’s just zis library, you know?"}'

HTTP/1.1 200 OK
Content-Type: application/json
Date: Tue, 15 Jul 2014 22:15:03 GMT
Connection: keep-alive
Transfer-Encoding: chunked

{"author":"Matt Brennan","text":"Livewire’s just zis library, you know?"}

And it’s in the array now:

$ curl -i localhost:3000/quote/4

HTTP/1.1 200 OK
Content-Type: application/json
Date: Tue, 15 Jul 2014 22:15:23 GMT
Connection: keep-alive
Transfer-Encoding: chunked

{"author":"Matt Brennan","text":"Livewire’s just zis library, you know?"}

Finally, we’ll add quote deletion. This looks a lot like our single-quote route. We slice the quote ID out of the array, and simply return a 200 OK.

route.delete('/quote/:id', function(req) {
  if(quotes[req.params.id]) {
  	quotes.splice(req.params.id, 1);
  	return resp.ok();
  }
  return resp.notFound('Quote not found');
})

So, let’s DELETE the quote we just added.

$ curl -X DELETE -i localhost:3000/quote/4

HTTP/1.1 200 OK
Date: Tue, 15 Jul 2014 22:22:57 GMT
Connection: keep-alive
Transfer-Encoding: chunked

Thus concludes our whirlwind tour of Livewire. In fact, I’ve covered pretty much everything it does. Livewire and its sibling libraries are designed to be solid bases for more complex functionality. For an example, have a look at Sodor, which is a Controller-like interface to Livewire’s routes. In this very post, we’ve used a wrapper library: Dram is based on the more low-level Peat, which is the library Oban itself uses. With raw Peat, we’d be writing [Status(200), "hello"] instead of ok("hello").

The code backing this tutorial is on Github. For inspiration, examples, and documentation, have a look at Livewire, Highland, Dram, Oban and Peat.