REST Framework Survey (Java, Haskell, Go, Node.js)

Over the last few days I’ve experimented with various REST frameworks. The initial goal was to find The Language and framework to use for all future projects… . Of course there is no clear winner.

These were the contestants:

The client was written in Go because I wanted it to be fast and easy to make concurrent requests to try Go.

The server spec is to povide a way to insert, query, update, list and delete documents.

  • A POST to /documents should create a new document, generate a new UUID (v4) as ID and return the whole document.
  • A GET to /documents should return a list of all documents
  • A GET to /documents/[ID] should return the document with the given ID, or 404 if it is not found.
  • A PUT to /documents/[ID] should update the document with the given ID and return it.
  • A DELETE to /documents/[ID] should delete the document with the given ID.

The documents are encoded in JSON:

{
    "id": "some id",
    "title": "some title",
    "text": "some text"
}

The client may or may not send the id field, the server ignores it and either generates a new one, or uses the one from the URL.

The client’s steps:

  1. Insert a document
  2. Update that document
  3. Delete that document

I used goroutines to make that concurrent and checked the consistency via a final GET /documents asserting an empty list.

Step 1

As database backend, I used an SQLite3 in-memory database connected via:

The implementation was easy with Java and Node.js. I’ve had my fair share of trouble with Haskell because there is no clear REST framework to use, at least I couldn’t find the obvious choice and went with Scotty because it’s simple and did what I needed, and my Haskell-fu has seen better days.

Here’s a inconclusive performance report1 (how long it took, in seconds, so longer is worse) processing 10000 documents with the same client on each of the backends.

Node.js is a bit slower, Java and Haskell are pretty much on par.

Code

Here’s the code used to update (PUT) the new version of a document on the server. I’ve chosen the update case because it shows how to deal with path-parameters, as well as how to decode and encode JSON.

Node.js Code to wire PUT requests to the DB.

server.put('/documents/:docId', update); // define which function to call for that URL+method
...
function update(req, res, next) {
  var doc = req.body; // directly available as object through the BodyParser plugin
  doc.id = req.params.docId; // taken from the URL
  db.run("update documents set title = ?, text = ? where id = ?", doc.title, doc.text, doc.id);
  return read(req, res, next); // read the document or return 404
}

The Jersey annotations used to declare “updateDocument” as handler for PUT requests. JSON en/decoding is fully transparent (because of the @Produces annotation).

@PUT // handle PUT requests
@Path("/documents/{id}") // on that URL
@Consumes(MediaType.APPLICATION_JSON) // accepts only JSON
@Produces(MediaType.APPLICATION_JSON) // writes out JSON
// params can be references and injected, the body is automaticall decoded into the correct object
public Document updateDocument(@PathParam("id") String id, Document document) throws SQLException {
    // perform the database stuff (in a data access object as it is customary in Java)
    Document document = documentDao.update(id, document);
    if (document == null) {
        throw new NotFoundException(); // produces the 404 status
    }
    // the returned object is also automatically encoded
    // and all headers are set correctly
    return document;
}

The Haskell version is very concise, but with the various Monad-layerings a bit opaque (especially the DB code):

put "/documents/:id" $ do -- the URL this function is defined for
  id <- param "id" -- extract the parameter from the URL
  inputDocument <- jsonData -- parse the JSON body
  doc <- liftIO $ updateDocument conn id inputDocument -- write the stuff into the DB
  resultOr404 doc -- return the document or 404

-- for reference on how to deal with Maybe
resultOr404 :: Maybe Document -> ActionM ()
resultOr404 Nothing  = status status404 -- return 404 without a body
resultOr404 (Just a) = json a -- return JSON (also setting the content type)

Actually, the only server checking for the existance of the document is the Haskell variant. Simply because the type system enforces it!

Finally, here is the corresponding Go client code:

func updateDocument(id string) {
    doc := Document{id, "New Title", "New Text"} // set the new content
    jsonReader := getJsonReader(doc) // encode into JSON
    req, _ := http.NewRequest("PUT", base + "/" + id, jsonReader) // prepare the call
    req.Header.Add("Content-Type", jsonType) // set the correct content-type
    res, _ := client.Do(req) // execute the call
    if res != nil { // check for a response
        res.Body.Close() // close the response stream
    }
    // yeah, I ignore errors...
}

Step 2

After trying out the services with the in-memory database, I got curious and wanted to see how they’d perform using a PostgreSQL Database. So I switched the database layer to these:

The switch was pretty easy for Java and Haskell (a matter of exchanging the database driver and connect-string). For Node.js I had to rewrite more or less the whole app since there seems to be no standard interface for DB access.

The client does not need to change.

The performance of the REST services with Postgres instead of SQLite are shown below. This time they show the time needed to process 1000 documents. I wasn’t patient enough to wait for 10000 documents to finish – I took the average of three runs.

This time, Node blew Java away, Haskell was also significantly slower than Node. My guess is, that the JDBC and HDBC abstractions take their fair share of overhead, but Java’s extreme case might have another cause (I haven’t investigated here).

Conclusion

Writing v1

In general, writing the server in Java was quite easy (I’m used to that), Node.js was easy too (chaning was hard), and Haskell took me more time than the other two servers and the Go client together. I’d love to add Clojure and a Go server to the mix and see how they perform.

All three contestants work well and are reasonably fast. I never noticed Java’s problem with Postgres as shown here, but I rarely use JDBC directly these days.

Update to v2

The switch from SQLite to Postgres was pretty painful in Node.js. For Java and Haskell the switch was easy and fast.

Deployment

To deploy a Java webapp, you need a servlet container (I used Jetty) and deploy it within that. The war file usually includes all necessary dependencies. Every Node.js app starts its own server on its own port, so there isn’t much to it: install Node.js, install the required libraries with npm, Profit! Haskell produces a single binary which can be copied to the destination. Starting the binary, also starts the webserver and can thus be used directly or via a proxy. There are still some dynamic linked libraries which have to be present on the target machine.

The nice thing about Java is, that all necessary dependencies are bundled with the app itself. Haskell and its type system also check whether newer versions are still compatible, for Node.js it seems that our only option is to perform adequate testing.

Summary

This sums up my endeavour: I’ve had the most fun writing the Go client, but the Haskell services has the fewest possibilities for error. Haskell is really, really cool for writing REST services, and performs very good. Node.js is very young and provides very good productivity and performance to get something up and running, but (to me), maintaining Node.js code seems like something I wouldn’t want to do. Java is a compromise, nobody got every fired for using Java (except, maybe someone on the Android team).

  1. What else to do with three similar REST services?

SQL, Lisp, and Haskell are the only programming languages that I’ve seen where one spends more time thinking than typing.
— Philip Greenspun