While I started dusting of an old project that I never properly finished, a crud adapter for Spray, I realized this is a perfect first topic for a blog post.
So this will be the first post in a series on akka-http, focussing on the routing DSL.
This first post will introduce the mental model of the routing DSL, introducing Directive
s and Route
s.
akka-http
Akka-http is a popular http library in the typesafe ecosystem used to embed http servers and clients into an application. It is a continuation of the Spray library and is largely the same as it’s predecesor. As a whole, akka-http is built as 2 layers and some utility modules for json and xml serialization and deserialization. The lower-level layer, or the low level API is an implementation of the http protocol and support to create servers and clients. These run on top of akka-stream but this is largely hidden from the user. Now when you’re working on an API, most of your time will likely be spent working with the high-level API, the very recognizable routing dsl.
Routing
When writing a server with akka-http, the routing is all written in plain Scala as opposed to a text file in for example Play. This comes with all the advantages and responsibilities of adding code to a project. It gives you type safety and allows for abstraction, more on this in a later post. On the flip side, you are responsible for the structure and organization of the routing code.
Enough introduction already! Let’s look at an example.
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model._
import akka.http.scaladsl.server.Directives._
import akka.stream.ActorMaterializer
object WebServer extends App {
implicit val system = ActorSystem()
implicit val materializer = ActorMaterializer()
val route =
get {
complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "Hello world!"))
}
Http().bindAndHandle(route, "localhost", 8080)
}
This snippet creates server that responds with "Hello world!"
to every GET
request.
This behaviour is defined by passing route
to bindAndHandle
.
Yay~ we got Hello world working. Let’s see what else we can do.
val route = pathPrefix("accounts") {
pathEndOrSingleSlash {
get {
val getAccountIds: Future[List[Int]] = getAccountIdsFromDatabase()
onSuccess(accountIds) { ids =>
complete(ids)
}
}
} ~ path(LongNumber) { id =>
get {
val getAccount: Future[Option[Account]] = getAccountFromDatabase(id)
onSuccess(getAccount) {
case Some(account) => complete(account)
case None => complete(StatusCodes.NotFound)
}
}
}
}
This route is starting to get a little more interesting, it describes a (for simplicity) read-only accounts
resource.
GET
requests to to "accounts"
respond with a list of available ids and GET
requests to "accounts/<id>"
respond with an account if it’s available.
This might look a little unfamiliar, you can think of this route as a tree that gets traversed depth first where each node in the tree is matching the incoming request or extracting data from it.
If the request can’t be matched all the way to a leave of the tree a default 404
response is sent
Directives and Routes
The route from the example can be read as such:
└── Does the first unmatched part of the path start with "accounts"?
├── Has the whole path been matched?
│ └── Is it a get request?
│ └── Then respond with the result of `getAccountIdsFromDatabase`
└── Can the rest of the path be parsed as a Long? Then bind the value of that long to id
└── Is it a get request?
└── Then respond with the account if available otherwise respond with 404
The questions, such as Is it a GET request?
or get
are directives.
All the leave nodes are routes, so that is all the complete
s.
Also, each path to a leave node is a route, for example get(path("hello")(complete("world")))
is a route that responds to ‘GET’ requests to /hello
with the response “world”.
More about the unmatched parts of paths later.
More formally, a route is defined as type Route = RequestContext ⇒ Future[RouteResult]
.
Let’s take a closer look at what a RequestContext
and a RouteResult
are.
trait RequestContext {
/** The request this context represents. Modelled as a `val` so as to enable an `import ctx.request._`. */
val request: HttpRequest
/** The unmatched path of this context. Modelled as a `val` so as to enable an `import ctx.unmatchedPath._`. */
val unmatchedPath: Uri.Path
...
}
What request
is obvious but unmatchedPath
deserves a bit more attention.
This is how directives like path
can match pieces of the path and be composed.
For example a request to /api/members
against
path("api"){
path("members"){
// members resource
???
}
}
will first match path("api")
with a requestContext with unmatchedPath /api/members
and then try to match with the inner directives using the part of the path that has not yet been been matched being /members
and RouteResult
represents the response.
Lastly Directive
, the main building block.
abstract class Directive[L](implicit val ev: Tuple[L]) {
/**
* Calls the inner route with a tuple of extracted values of type `L`.
*
* `tapply` is short for "tuple-apply". Usually, you will use the regular `apply` method instead,
* which is added by an implicit conversion (see `Directive.addDirectiveApply`).
*/
def tapply(f: L ⇒ Route): Route
...
}
As seen from the comment in the snippet above, a directive takes a function from something that can be seen as a tuple to a Route
, resulting in a Route
.
For example in the route entity(as[String])(body => complete(body))
, entity(as)
extracts the body from the request and brings it in scope as body
as a String
which is then echoed back through the use Route
complete(body)
.
Summary
We have now explored the basic abstractions that are used in akka-http’s routing dsl, Directive
and Route
.
Together they are used to create a Route
tree which you can use to extract data from the request and respond accordingly.
Next time a closer look at ways to compose Directive
s.