The "Route" is the central concept of Akka HTTP's Routing DSL. All the structures you build with the DSL, no matter whether they consists of a single line or span several hundred lines, are instances of this type:

type Route = RequestContext  Future[RouteResult]

It's a simple alias for a function turning a RequestContext into a Future[RouteResult].

Generally when a route receives a request (or rather a RequestContext for it) it can do one of these things:

  • Complete the request by returning the value of requestContext.complete(...)
  • Reject the request by returning the value of requestContext.reject(...) (see Rejections)
  • Fail the request by returning the value of or by just throwing an exception (see Exception Handling)
  • Do any kind of asynchronous processing and instantly return a Future[RouteResult] to be eventually completed later

The first case is pretty clear, by calling complete a given response is sent to the client as reaction to the request. In the second case "reject" means that the route does not want to handle the request. You'll see further down in the section about route composition what this is good for.

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 from the Low-Level Server-Side API.

Note: There is also an implicit conversion from Route to Flow[HttpRequest, HttpResponse, Unit] defined in the RouteResult companion, which relies on Route.handlerFlow.


The request context 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. It also contains the unmatchedPath, a value that describes how much of the request URI has not yet been matched by a Path Directive.

The RequestContext itself is immutable but contains several helper methods which allow for convenient creation of modified copies.


RouteResult is a simple abstract data type (ADT) that models the possible non-error results of a Route. It is defined as such:

sealed trait RouteResult

object RouteResult {
  final case class Complete(response: HttpResponse) extends RouteResult
  final case class Rejected(rejections: immutable.Seq[Rejection]) extends RouteResult

Usually you don't create any RouteResult instances yourself, but rather rely on the pre-defined RouteDirectives (like complete, reject or redirect) or the respective methods on the RequestContext instead.

Composing Routes

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

The last point is achieved with the concatenation operator ~, which is an extension method that becomes available when you import akka.http.scaladsl.server.Directives._. The first two points are provided by so-called Directives of which a large number is already predefined by Akka HTTP and which you can also easily create yourself. Directives deliver most of Akka HTTP's power and flexibility.

The Routing Tree

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.

Consider this schematic example:

val route =
  a {
    b {
      c {
        ... // route 1
      } ~
      d {
        ... // route 2
      } ~
      ... // route 3
    } ~
    e {
      ... // route 4

Here five directives form a routing tree.

  • Route 1 will only be reached if directives a, b and c all let the request pass through.
  • Route 2 will run if a and b pass, c rejects and d passes.
  • Route 3 will run if a and b pass, but c and d reject.

Route 3 can therefore be seen as a "catch-all" route that only kicks in, if routes chained into preceding positions reject. This mechanism can make complex filtering logic quite easy to implement: simply put the most specific cases up front and the most general cases in the back.