HTTP Server logic

Class QuickstartServer is intended to “bring it all together”, it is the main class that will run the application, as well as the class that should bootstrap all actors and other dependencies (database connections etc).

public class QuickstartServer extends AllDirectives {

    // set up ActorSystem and other dependencies here
    private final UserRoutes userRoutes;

    public QuickstartServer(ActorSystem system, ActorRef userRegistryActor) {
        userRoutes = new UserRoutes(system, userRegistryActor);
    }
    /**
     * Here you can define all the different routes you want to have served by this web server
     * Note that routes might be defined in separated classes like the current case
     */
    protected Route createRoute() {
        return userRoutes.routes();
    }
}

Notice that we’ve separated out the UserRoutes class, in which we’ll put all our actual route definitions. This is a good pattern to follow, especially once your application starts to grow and you’ll need some form of compartmentalizing them into groups of routes handling specific parts of the exposed API.

Binding endpoints

Each Akka HTTP Route contains one or more akka.http.javadsl.server.AllDirectives, such as: path, get, post, complete, etc. There is also a low-level API that allows to inspect requests and create responses manually. For the user registry service, the example needs to support the actions listed below. For each, we can identify a path, the HTTP method, and return value:

Functionality HTTP Method Path Returns
Create a user POST /users Confirmation message
Retrieve all users GET /users JSON payload
Retrieve a user GET /users/$ID JSON payload
Remove a user DELETE /users/$ID Confirmation message

The top-level Route

A Route is constructed by nesting various directives which route an incoming request to the appropriate handler block.

Below is the top-level Route definition that gives a high-level structure to the routes but delegates the specifics to individual functions:

public Route routes() {
    return route(pathPrefix("users", () ->
        route(
            getOrPostUsers(),
            path(PathMatchers.segment(), name -> route(
                getUser(name),
                deleteUser(name)
              )
            )
        )
    ));
}

We will look at how these are built up in more detail in the next couple of sections.

Retrieving and creating users

The definition of the endpoint to retrieve and create users look like the following:

private Route getOrPostUsers() {
    return pathEnd(() ->
        route(
            get(() -> {
                CompletionStage<UserRegistryActor.Users> futureUsers = Patterns
                    .ask(userRegistryActor, new UserRegistryMessages.GetUsers(), timeout)
                    .thenApply(UserRegistryActor.Users.class::cast);
                return onSuccess(() -> futureUsers,
                    users -> complete(StatusCodes.OK, users, Jackson.marshaller()));
            }),
            post(() ->
                entity(
                    Jackson.unmarshaller(User.class),
                    user -> {
                        CompletionStage<ActionPerformed> userCreated = Patterns
                            .ask(userRegistryActor, new UserRegistryMessages.CreateUser(user), timeout)
                            .thenApply(ActionPerformed.class::cast);
                        return onSuccess(() -> userCreated,
                            performed -> {
                                log.info("Created user [{}]: {}", user.getName(), performed.getDescription());
                                return complete(StatusCodes.CREATED, performed, Jackson.marshaller());
                            });
                    }))
        )
    );
}

Generic functionality

The following directives are used in the above example:

  • pathPrefix("users") : the path that is used to match the incoming request against.
  • pathEnd : used on an inner-level to discriminate “path already fully matched” from other alternatives. Will, in this case, match on the “users” path.
  • route: concatenates two or more route alternatives. Routes are attempted one after another. If a route rejects a request, the next route in the chain is attempted. This continues until a route in the chain produces a response. If all route alternatives reject the request, the concatenated route rejects the route as well. In that case, route alternatives on the next higher level are attempted. If the root level route rejects the request as well, then an error response is returned that contains information about why the request was rejected.

Retrieving users

  • get : matches against GET HTTP method.
  • complete : completes a request which means creating and returning a response from the arguments.

Creating a user

  • post : matches against POST HTTP method.
  • entity(...) : converts the HTTP request body into a domain object of type User. Implicitly, we assume that the request contains application/json content. We will look at how this works in the JSON section.
  • complete : completes a request which means creating and returning a response from the arguments. Note, that call returns a response with the given status code and a text/plain body with the given string along with explicit marshaller as last parameter.

Retrieving and removing a user

Next, the example defines how to retrieve and remove a user. In this case, the URI must include the user’s id in the form: /users/$ID. See if you can identify the code that handles that in the following snippet. This part of the route includes logic for both the GET and the DELETE methods.

public Route routes() {
    return route(pathPrefix("users", () ->
        route(
            getOrPostUsers(),
            path(PathMatchers.segment(), name -> route(
                getUser(name),
                deleteUser(name)
              )
            )
        )
    ));
}

private Route getUser(String name) {
  return get(() -> {
      CompletionStage<Optional<User>> maybeUser = Patterns
              .ask(userRegistryActor, new UserRegistryMessages.GetUser(name), timeout)
              .thenApply(Optional.class::cast);

      return onSuccess(() -> maybeUser,
          performed -> {
              if (performed.isPresent())
                  return complete(StatusCodes.OK, performed.get(), Jackson.marshaller());
              else
                  return complete(StatusCodes.NOT_FOUND);
          }
        );
    });
}

private Route deleteUser(String name) {
  return
      delete(() -> {
        CompletionStage<ActionPerformed> userDeleted = Patterns
          .ask(userRegistryActor, new UserRegistryMessages.DeleteUser(name), timeout)
          .thenApply(ActionPerformed.class::cast);

        return onSuccess(() -> userDeleted,
          performed -> {
            log.info("Deleted user [{}]: {}", name, performed.getDescription());
            return complete(StatusCodes.OK, performed, Jackson.marshaller());
          }
        );
      });
}

Generic functionality

The following directives are used in the above example:

  • pathPrefix("users") : the path that is used to match the incoming request against.
  • route: concatenates two or more route alternatives. Routes are attempted one after another. If a route rejects a request, the next route in the chain is attempted. This continues until a route in the chain produces a response.
  • path(PathMatchers.segment(), name -> : this bit of code matches against URIs of the exact format /users/$ID and the Segment is automatically extracted into the name variable so that we can get to the value passed in the URI. For example /users/Bruce will populate the name variable with the value “Bruce.” There is plenty of more features available for handling of URIs, see pattern matchers for more information.

Retrieving a user

  • get : matches against GET HTTP method.
  • complete : completes a request which means creating and returning a response from the arguments.

Let’s break down the logic handling the incoming request:

CompletionStage<Optional<User>> maybeUser = Patterns
        .ask(userRegistryActor, new UserRegistryMessages.GetUser(name), timeout)
        .thenApply(Optional.class::cast);

return onSuccess(() -> maybeUser,
    performed -> {
        if (performed.isPresent())
            return complete(StatusCodes.OK, performed.get(), Jackson.marshaller());
        else
            return complete(StatusCodes.NOT_FOUND);
    }
  );

The rejectEmptyResponse here above is a convenience method that automatically unwraps a CompletionStage, handles an Optional by converting value into a successful response, returns a HTTP status code 404 if value is not present, and passes on to the ExceptionHandler in case of an error, which returns the HTTP status code 500 by default.

Deleting a user

  • delete : matches against the Http directive DELETE.

The logic for handling delete requests is as follows:

delete(() -> {
  CompletionStage<ActionPerformed> userDeleted = Patterns
    .ask(userRegistryActor, new UserRegistryMessages.DeleteUser(name), timeout)
    .thenApply(ActionPerformed.class::cast);

  return onSuccess(() -> userDeleted,
    performed -> {
      log.info("Deleted user [{}]: {}", name, performed.getDescription());
      return complete(StatusCodes.OK, performed, Jackson.marshaller());
    }
  );
});

So we send an instruction about removing a user to the user registry actor, wait for the response and return an appropriate HTTP status code to the client.

Binding the HTTP server

At the beginning of the main class, the example defines some implicit values that will be used by the Akka HTTP server:

// boot up server using the route as defined below
ActorSystem system = ActorSystem.create("helloAkkaHttpServer");

final Http http = Http.get(system);
final ActorMaterializer materializer = ActorMaterializer.create(system);

Akka Streams uses these values:

  • ActorSystem : provides a context in which actors will run. What actors, you may wonder? Akka Streams uses actors under the hood, and the actor will be picked up and used by Streams.
  • ActorMaterializer : while the ActorSystem is the host of all thread pools and live actors, an ActorMaterializer is specific to Akka Streams and is what makes them run. The ActorMaterializer interprets stream descriptions into executable entities which are run on actors, and this is why it requires an ActorSystem to function.

Further down in QuickstartServer.java, you will find the code to instantiate the server:

//In order to access all directives we need an instance where the routes are define.
QuickstartServer app = new QuickstartServer(system, userRegistryActor);

final Flow<HttpRequest, HttpResponse, NotUsed> routeFlow = app.createRoute().flow(system, materializer);
http.bindAndHandle(routeFlow, ConnectHttp.toHost("localhost", 8080), materializer);

System.out.println("Server online at http://localhost:8080/");

The bindAndHandle method only takes three parameters; routes, the hostname with the port and materializer. That’s it! When this program runs, it starts an Akka HTTP server on localhost port 8080. Note that startup happens asynchronously and therefore the bindAndHandle method returns a CompletionStage.

The complete server code

Here is the complete server code used in the sample:

package $package$;

import akka.NotUsed;
import akka.actor.ActorRef;
import akka.actor.ActorSystem;
import akka.http.javadsl.ConnectHttp;
import akka.http.javadsl.Http;
import akka.http.javadsl.model.HttpRequest;
import akka.http.javadsl.model.HttpResponse;
import akka.http.javadsl.server.AllDirectives;
import akka.http.javadsl.server.Route;
import akka.stream.ActorMaterializer;
import akka.stream.javadsl.Flow;

public class QuickstartServer extends AllDirectives {

    // set up ActorSystem and other dependencies here
    private final UserRoutes userRoutes;

    public QuickstartServer(ActorSystem system, ActorRef userRegistryActor) {
        userRoutes = new UserRoutes(system, userRegistryActor);
    }

    public static void main(String[] args) throws Exception {
        // boot up server using the route as defined below
        ActorSystem system = ActorSystem.create("helloAkkaHttpServer");

        final Http http = Http.get(system);
        final ActorMaterializer materializer = ActorMaterializer.create(system);

        ActorRef userRegistryActor = system.actorOf(UserRegistryActor.props(), "userRegistryActor");

        //In order to access all directives we need an instance where the routes are define.
        QuickstartServer app = new QuickstartServer(system, userRegistryActor);

        final Flow<HttpRequest, HttpResponse, NotUsed> routeFlow = app.createRoute().flow(system, materializer);
        http.bindAndHandle(routeFlow, ConnectHttp.toHost("localhost", 8080), materializer);

        System.out.println("Server online at http://localhost:8080/");
    }

    /**
     * Here you can define all the different routes you want to have served by this web server
     * Note that routes might be defined in separated classes like the current case
     */
    protected Route createRoute() {
        return userRoutes.routes();
    }
}

Let’s move on to the actor that handles registration.