This is a note for learning Akka HTTP based on the official Akka HTTP document. The Akka HTTP module is a toolkit that implements both server-side and client-side HTTP stack on top of akka-actor and akka-stream. It has serveral API levels.

Introduction

The goal of Akka HTTP is to build an integration layer rather than an application core. It is a library, not a framework.

The Route is created using the Route DSL and is then bound to a port to serve HTTP requests.

Akka HTTP’s marshalling and unmarshalling functionality converts application-domain objects from and to JSON. Integration with spray-json is provided out of the box through the akka-http-spray-json module.

Akka HTTP supports streaming response with backpressure. Its routes easily interact with actors.

The low-level APIs accept HttpRequest and answer HttpResponse. The client APIs provide methods for calling a HTTP server using the same HttpRequest and HttpResponse. Akka has extensions for generating/consuming Swagger specfication and support client sessions.

Marshalling, Unmarshalling and Json Support

Marshalling converts higher-level structure into lower-level representation, also is called serialization or pickling. Marshalling of instances of type A into instances of type B is performed by a Marshaller[A, B] that produces:

  • A Future: it is an asynchronous processing.
  • of List: it is a list of different content type. Which one will be rendered is decided by content negotiation.
  • of Marshalling[B]: it has meta-data such as MediaType or HttpCharset.

Unmarshalling converts lower-level representation into higher-level structure. It is also called deserialization or unpickling. Unmarshaller[A, B] is very similar to a function A => Future[B].

Akka HTTP integrates with spray-json to convert domain objects from and to JSON. The SprayJsonSupport trait provides a FromEntityUnmarshaller[T] and ToEntityMarshaller[T] for every type T that an implicit spray.json.RootJsonReader and/or spray.json.RootJsonWriter (respectively) is available for. Just provide a RootJsonFormat[T] for your type and bring it into scope. You can also use Json streaming.

Routing DSL

The high-level routing DSL provides routing, URI deconstruction, content negotiation and static content serving.

Routing DSL expresses service behavior as a structure of composable elements called Directives. Directives are assembled into a route structure which creates a handler flow.

The Route is the central concept of Akka routing DSL. It converts a RequestContext into a Future[RouteResult]. It performs one of the following things:

  • Complete the request via requestContext.complete().
  • Reject the request via requestContext.reject(...).
  • Fail the request via requestContext.faile(...) or throwing an exception.
  • Perform asynchronous processing and return a Future[RouteResult] to be eventually completed later.

The RequestContext wraps an HttpRequest instance to enrich it with additional information that are typically required by the routing logic, like an ExecutionContext, Materializer, LoggingAdapter and the configured RoutingSettings.

A Route can be “sealed” using Route.seal, which relies on the in-scope RejectionHandler and ExceptionHandler instances to convert rejections and exceptions into appropriate HTTP responses for the client. Using Route.handlerFlow or Route.asyncHandler a Route can be lifted into a handler Flow or async handler function to be used with a bindAndHandleXXX call.

There are three basic operations we need for building more complex routes from simpler ones:

  • Route transformation, which delegates processing to another, “inner” route but in the process changes some properties of either the incoming request, the outgoing response or both.
  • Route filtering, which only lets requests satisfying a given filter condition pass and rejects all others.
  • Route chaining, which tries a second route if a given first one was rejected.

Essentially, when you combine directives and custom routes via nesting and the ~ operator, you build a routing structure that forms a tree. When a request comes in it is injected into this tree at the root and flows down through all the branches in a depth-first manner until either some node completes it or it is fully rejected.

Directives

Directives create Routes. A route is a function that take a RequestContext and complete it, often asynchronously. The following three ways have the same effects:

  • val route: Route = { ctx => ctx.complete("yeah") }
  • val route: Route = _.complete("yeah")
  • val route = complete("yeah")

The general anatomy of a directive is as follows:

1
2
3
name(arguments) { extractions =>
  ... // inner route
}

A directive has a name, zero or more arguments and optionally an inner route. The RouteDirective is always used at the leaf-level and cannot have inner routes. A directive can do one or more of the following:

  • Transform the incoming RequestContext before passing it on to its inner route (i.e. modify the request)
  • Filter the RequestContext according to some logic, i.e. only pass on certain requests and reject others
  • Extract values from the RequestContext and make them available to its inner route as “extractions”
  • Chain some logic into the RouteResult future transformation chain (i.e. modify the response or rejection)
  • Complete the request

Directives are composed using concat(a, b, c) or a ~ b ~ c. Directives can be combined using & such as val order = path("order" / IntNumber) & (get | put) & extractMethod. Routing DSL supports tuple extraction. You can create custom directives if the predefined directives don’t meet your need.