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:
- Custom
Cache-Control
header generation. Achieve optimal caching by tailoring a resource'smax-age
to avoid over- or under-caching individual resources. - Separate on-the-wire protocol. I've seen code bases where the domain objects contain more annotations than code—often for both JSON and ORM mappings. I think this is bad practice and prefer to use different classes.
- The
onSuccess
directive, which allows us to improve on my previous post on handling expected errors (e.g. 404) in the spray routing DSL.
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.
;