Routing DSL Overview

The Apache Pekko HTTP Core Server API provides a FlowFlow- or Function-level interface that allows an application to respond to incoming HTTP requests by mapping requests to responses (excerpt from Low-level server side example):

Scala
source/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * license agreements; and to You under the Apache License, version 2.0:
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * This file is part of the Apache Pekko project, which was derived from Akka.
 */

/*
 * Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
 */

package docs.http.scaladsl

import org.apache.pekko
import pekko.actor.typed.ActorSystem
import pekko.actor.typed.scaladsl.Behaviors
import pekko.http.scaladsl.Http
import pekko.http.scaladsl.model.HttpMethods._
import pekko.http.scaladsl.model._

import scala.concurrent.ExecutionContext
import scala.io.StdIn

object HttpServerLowLevel {

  def main(args: Array[String]): Unit = {
    implicit val system = ActorSystem(Behaviors.empty, "lowlevel")
    // needed for the future map/flatmap in the end
    implicit val executionContext: ExecutionContext = system.executionContext

    val requestHandler: HttpRequest => HttpResponse = {
      case HttpRequest(GET, Uri.Path("/"), _, _, _) =>
        HttpResponse(entity = HttpEntity(
          ContentTypes.`text/html(UTF-8)`,
          "<html><body>Hello world!</body></html>"))

      case HttpRequest(GET, Uri.Path("/ping"), _, _, _) =>
        HttpResponse(entity = "PONG!")

      case HttpRequest(GET, Uri.Path("/crash"), _, _, _) =>
        sys.error("BOOM!")

      case r: HttpRequest =>
        r.discardEntityBytes() // important to drain incoming HTTP Entity stream
        HttpResponse(404, entity = "Unknown resource!")
    }

    val bindingFuture = Http().newServerAt("localhost", 8080).bindSync(requestHandler)
    println(s"Server online at http://localhost:8080/\nPress RETURN to stop...")
    StdIn.readLine() // let it run until user presses return
    bindingFuture
      .flatMap(_.unbind()) // trigger unbinding from the port
      .onComplete(_ => system.terminate()) // and shutdown when done

  }
}
Java
sourcefinal Function<HttpRequest, HttpResponse> requestHandler =
    new Function<HttpRequest, HttpResponse>() {
      private final HttpResponse NOT_FOUND =
          HttpResponse.create().withStatus(404).withEntity("Unknown resource!");

      @Override
      public HttpResponse apply(HttpRequest request) throws Exception {
        Uri uri = request.getUri();
        if (request.method() == HttpMethods.GET) {
          if (uri.path().equals("/")) {
            return HttpResponse.create()
                .withEntity(
                    ContentTypes.TEXT_HTML_UTF8, "<html><body>Hello world!</body></html>");
          } else if (uri.path().equals("/hello")) {
            String name = uri.query().get("name").orElse("Mister X");

            return HttpResponse.create().withEntity("Hello " + name + "!");
          } else if (uri.path().equals("/ping")) {
            return HttpResponse.create().withEntity("PONG!");
          } else {
            return NOT_FOUND;
          }
        } else {
          return NOT_FOUND;
        }
      }
    };

While it’d be perfectly possible to define a complete REST API service purely by pattern-matching againstinspecting the incoming HttpRequestHttpRequest (maybe with the help of a few extractors in the way of Unfiltered) this approach becomes somewhat unwieldy for larger services due to the amount of syntax “ceremony” required. Also, it doesn’t help in keeping your service definition as DRY as you might like.

As an alternative Apache Pekko HTTP provides a flexible DSL for expressing your service behavior as a structure of composable elements (called Directives) in a concise and readable way. Directives are assembled into a so called route structure which, at its top-level, can be used to create a handler FlowFlow or async handler function that can be directly supplied to a bind call. The conversion from Route to flow can either be invoked explicitly using Route.toFlow or, otherwise, the conversion is also provided implicitly by RouteResult.routeToFlow [1].

Here’s the complete example rewritten using the composable high-level API:

Scala
source/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * license agreements; and to You under the Apache License, version 2.0:
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * This file is part of the Apache Pekko project, which was derived from Akka.
 */

/*
 * Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
 */

package docs.http.scaladsl

import org.apache.pekko
import pekko.actor.ActorSystem
import pekko.http.scaladsl.Http
import pekko.http.scaladsl.model.{ ContentTypes, HttpEntity }
import pekko.http.scaladsl.server.Directives._
import scala.io.StdIn

object HttpServerHighLevel {
  def main(args: Array[String]): Unit = {
    implicit val system = ActorSystem()
    // needed for the future flatMap/onComplete in the end
    implicit val executionContext = system.dispatcher

    val route =
      get {
        concat(
          pathSingleSlash {
            complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<html><body>Hello world!</body></html>"))
          },
          path("ping") {
            complete("PONG!")
          },
          path("crash") {
            sys.error("BOOM!")
          })
      }

    // `route` will be implicitly converted to an async handler
    val bindingFuture = Http().newServerAt("localhost", 8080).bind(route)
    println(s"Server online at http://localhost:8080/\nPress RETURN to stop...")
    StdIn.readLine() // let it run until user presses return
    bindingFuture
      .flatMap(_.unbind()) // trigger unbinding from the port
      .onComplete(_ => system.terminate()) // and shutdown when done
  }
}
Java
source
import org.apache.pekko.actor.ActorSystem; import org.apache.pekko.http.javadsl.Http; import org.apache.pekko.http.javadsl.ServerBinding; import org.apache.pekko.http.javadsl.model.ContentTypes; import org.apache.pekko.http.javadsl.model.HttpEntities; import org.apache.pekko.http.javadsl.server.AllDirectives; import org.apache.pekko.http.javadsl.server.Route; import java.io.IOException; import java.util.concurrent.CompletionStage; public class HighLevelServerExample extends AllDirectives { public static void main(String[] args) throws IOException { // boot up server using the route as defined below ActorSystem system = ActorSystem.create(); final HighLevelServerExample app = new HighLevelServerExample(); final Http http = Http.get(system); final CompletionStage<ServerBinding> binding = http.newServerAt("localhost", 8080).bind(app.createRoute()); System.out.println("Type RETURN to exit"); System.in.read(); binding.thenCompose(ServerBinding::unbind).thenAccept(unbound -> system.terminate()); } public Route createRoute() { // This handler generates responses to `/hello?name=XXX` requests Route helloRoute = parameterOptional( "name", optName -> { String name = optName.orElse("Mister X"); return complete("Hello " + name + "!"); }); return // here the complete behavior for this server is defined // only handle GET requests get( () -> concat( // matches the empty path pathSingleSlash( () -> // return a constant string with a certain content type complete( HttpEntities.create( ContentTypes.TEXT_HTML_UTF8, "<html><body>Hello world!</body></html>"))), path( "ping", () -> // return a simple `text/plain` response complete("PONG!")), path( "hello", () -> // uses the route defined above helloRoute))); } }

The core of the Routing DSL becomes available with a single import:

Scala
import org.apache.pekko.http.scaladsl.server.Directives._
Java
import static org.apache.pekko.http.javadsl.server.Directives.*;

Or by extending the org.apache.pekko.http.javadsl.server.AllDirectives class which brings together all directives into a single class for easier access:

extends AllDirectives

Of course it is possible to directly import only the directives you need (i.e. WebSocketDirectivesWebSocketDirectives etc).

This example also relies on the pre-defined support for Scala XML with:

import org.apache.pekko.http.scaladsl.marshallers.xml.ScalaXmlSupport._

The very short example shown here is certainly not the best for illustrating the savings in “ceremony” and improvements in conciseness and readability that the Routing DSL promises. The Long Example might do a better job in this regard.

For learning how to work with the Routing DSL you should first understand the concept of Routes.

[1] To be picked up automatically, the implicit conversion needs to be provided in the companion object of the source type. However, as RouteRoute is just a type alias for RequestContext => Future[RouteResult], there’s no companion object for RouteRoute. Fortunately, the implicit scope for finding an implicit conversion also includes all types that are “associated with any part” of the source type which in this case means that the implicit conversion will also be picked up from RouteResult.routeToFlow automatically.