Creating Custom Directives in Akka HTTP

Miguel Lopez
Quick Code
Published in
5 min readAug 23, 2018

To tidy up your routes.

If you’ve ever written something more complex than a Hello World API in Akka HTTP, you might have found out that the routing DSL can get quickly out of hand.

With deeply nested layers in your routes, and maybe even some code duplication.

That’s what directives are for. But what is a directive?

Directives are small, composable building blocks that allow you to create your routes piece by piece.

A typical route looks like:

pathPrefix("todos") {
pathEndOrSingleSlash {
get {
onComplete(todoRepository.all()) {
case Success(todos) =>
complete(todos)
case Failure(exception) =>
println(exception.getMessage)
complete(ApiError.generic.statusCode, ApiError.generic.message)
}
}
}
}

The route above accepts GET requests in the path /todos or /todos/ . It also does some error handling when the future returned by todoRepository.all() completes.

However, we will need this error handling in most — if not all — of our routes. As it is right now, we would have to copy and paste it all around.

And this is only error handling, there might be other concerns that we’d like to handle in our routes, such as authentication.

So, how can we reuse those error handling bits?

We can create our own custom directives.

Setting up the project

We will work with a base project, make sure to clone it and checkout the 6.3-api-errors branch to follow along.

Before creating the custom directive, let’s do a quick overview of the project.

The project uses Scala 2.12.6 and SBT 1.1.6 , you can confirm the versions in the build.sbt and build.properties files respectively.

The project is an API for a todo application. Running tree src in the root level of the project gives us the following:

src
├── main
│ └── scala
│ ├── ApiError.scala
│ ├── Main.scala
│ ├── Router.scala
│ ├── Server.scala
│ ├── Todo.scala
│ └── TodoRepository.scala
└── test
└── scala
├── TodoMocks.scala
└── TodoRouterListSpec.scala
4 directories, 8 files

Briefly, the classes’ responsibilities are:

  • ApiError: model for errors.
  • Main: application entry point.
  • Router: where our routes live.
  • Server: takes a router and binds it to a host and port.
  • Todo: application model.
  • TodoRepository: handles the CRUD for todos.

Inside the TodoRouter implementation, you can see the route we discussed above, where we are doing some manual error handling, and we’d like to do the same in the other routes.

If you look at the build.sbt file, you can see the dependencies we need, the Akka actors, streams and HTTP modules, Circe for JSON and an extra library to use it with Akka HTTP.

Creating custom directives

There are multiple ways of creating custom directives, but we’ll transform existing directives to create new ones.

Create a new trait under src/main/scala named TodoDirectives , it will extend the trait Directives, and we also need to import Circe and its support library, which we are using for JSON serialization.

import akka.http.scaladsl.server.Directives

trait TodoDirectives extends Directives {
import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._
import io.circe.generic.auto._

}

Let’s create a generic function that will handle a future’s failure, given a function that decides which ApiError it should return when something unexpected occurs:

def handle[T](f: Future[T])(e: Throwable => ApiError): Directive1[T] =
onComplete(f) flatMap {
case Success(t) =>
provide(t)
case Failure(error) =>
val apiError = e(error)
complete(apiError.statusCode, apiError.message)
}

It’s pretty similar to error handling logic we have in our route, however let’s look at the differences.

We receive a function that receives a Throwable and returns an ApiError , this will allow us to handle this externally, and make the directive more generic.

Also, we use flatMap because we are transforming the directives to create new ones, we are not applying them.

If the future succeeds we provide the value, which is why our return type is a Directive1[T] , our directive will return or provide one value, which is the value contained in the future.

However, if the future fails we get the ApiError from the function, and we send the response back to the client with the respective status code and message.

If you look inside the ApiError.scala file, we only have one generic error. Right now our repository can’t fail in many ways because we are only listing todos.

Let’s create another directive, that no matter what the exception is, it will always reply back with the generic ApiError :

def handleWithGeneric[T](f: Future[T]): Directive1[T] =
handle[T](f)(_ => ApiError.generic)

Let’s use it to tidy up our error handling in our TodoRouter . First, we need to extend our TodoDirective trait to have access to our custom directives.

Then we can update the route, and it will look like:

pathPrefix("todos") {
pathEndOrSingleSlash {
get {
handleWithGeneric(todoRepository.all()) { todos =>
complete(todos)
}
}
} ~ ...

It’s cleaner than the initial one, more importantly, we can now update the other routes with less boilerplate. Our TodoRouter ends up looking like:

import akka.http.scaladsl.server.{Directives, Route}

trait Router {
def route: Route
}

class TodoRouter(todoRepository: TodoRepository) extends Router with Directives with TodoDirectives {
import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._
import io.circe.generic.auto._

override def route: Route = pathPrefix("todos") {
pathEndOrSingleSlash {
get {
handleWithGeneric(todoRepository.all()) { todos =>
complete(todos)
}
}
} ~ path("done") {
get {
handleWithGeneric(todoRepository.done()) { todos =>
complete(todos)
}
}
} ~ path("pending") {
get {
handleWithGeneric(todoRepository.pending()) { todos =>
complete(todos)
}
}
}
}
}

We can also make sure (and probably should) that our routes still work as expected by either running the TodoRouterListSpec test, or running sbt test from the command line inside the project’s root directory.

Running sbt test should give you an output similar to:

[info] TodoRouterListSpec:
[info] A TodoRouter
[info] - should return all the todos
[info] - should return all the done todos
[info] - should return all the pending todos
[info] - should handle repository failure in the todos route
[info] - should handle repository failure in the done todos route
[info] - should handle repository failure in the pending todos route
[info] Run completed in 1 second, 859 milliseconds.
[info] Total number of tests run: 6
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 6, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[success] Total time: 13 s, completed 20-Aug-2018 23:21:12

Which means that all the tests are still passing. Awesome! 👏🏽

If you liked the tutorial, we’ve got a free Akka HTTP course where you’ll build the Todo project we used here from scratch, explained step by step. See you inside! 👇🏽

--

--

Miguel Lopez
Quick Code

🚀 Developing microservices with Scala, Akka, Kafka and AWS at Disney Streaming Services. 💻 Free Akka HTTP course: http://link.codemunity.io/m-free-akka-course