Spray Example

I love using Spray and its routing DSL for REST APIs but I've found a lot of the examples are just showing off its syntax. I've been missing examples of how you can use it to wire together a real application. Therefore I put together an example that goes a bit beyond the routing syntax to document my current understanding of best practices in that regard.

The main goal of this example is to show how to wire together a REST API using Spray Routing (v1.2) that uses a model-actor from the spray routing layer. It also shows off three things I consider good practice:

Let's start with the ModelActor. It is really just a toy, because the purpose of this example is to show how you plug an actor-based model into spray routing. There's just one thing I want to point out: the Model has a method get(id: Int): Option[Item], but it is considered bad practice to send Some[Item] and None messages between actors. Actors have much more freedom when it comes to the types of messages it responds with so we transform the response from the model into Item or ItemNotFound as appropriate.

Next let's look at the Service. For clarity the following examples focus on a single point each, but the example project puts it all together. First, let's look at the onSuccess directive that lets us handle two different success cases (or one success and one expected error, depending on how you look at it) in stride:

onSuccess(model ? id) {
    case item: Item => complete(OK, item)
    case ItemNotFound => complete(NotFound)

How do we get the model into the route, while still keeping things easy to test? My preference is to make the route definition a def rather than a val and pass in the model actor from a very thin ServiceActor. This allows us to inject a test actor very simply and thus create routing tests very naturally.

As mentioned I like to use a separate on-the-wire protocol—in other words: separate DTO classes from the model classes. The counter-argument I've usually heard is that it's "unnecessary" work; but if you're dealing with immutable objects you're probably just copying the pointers to the members, so you're not copying that much data around. On the other hand it becomes a lot simpler to avoid accidental leakage of your model fields if you never pass the model objects across the wire in the first place.

You might wonder why I send model objects to the service layer at all: the reason has to do with caching. Items change state and availability depending on stock level, going from InStock to LowStock to SoldOut. We want to strike a balance between caching everything too long, or everything too short. To do that we use an item's stock level to decide how long to cache for. (To avoid leaking exact stock levels via the Cache-Control header's max-age we use a formula to obfuscate the levels a bit.) Note that for a list we must pick the minimum stock level in the list to use for the cache decider.

I think this gives a clean separation between the model / controller layer and the service layer. The model layer doesn't need to know that it is being exposed by a REST API at all.

Posted 16 February 2014 Copyright © Stig Brautaset