1. Overview

In this tutorial, with the help of Akka’s Actor & Stream models, we’ll learn how to set up Akka to create an HTTP API that provides basic CRUD operations.

2. Maven Dependencies

To start, let’s take a look at the dependencies required to start working with Akka HTTP:

<dependency>
    <groupId>com.typesafe.akka</groupId>
    <artifactId>akka-http_2.12</artifactId>
    <version>10.0.11</version>
</dependency>
<dependency>
    <groupId>com.typesafe.akka</groupId>
    <artifactId>akka-stream_2.12</artifactId>
    <version>2.5.11</version>
</dependency>
<dependency>
    <groupId>com.typesafe.akka</groupId>
    <artifactId>akka-http-jackson_2.12</artifactId>
    <version>10.0.11</version>
</dependency>
<dependency>
    <groupId>com.typesafe.akka</groupId>
    <artifactId>akka-http-testkit_2.12</artifactId>
    <version>10.0.11</version>
    <scope>test</scope>
</dependency>

We can, of course, find the latest version of these Akka libraries on Maven Central.

3. Creating an Actor

As an example, we’ll build an HTTP API that allows us to manage user resources. The API will support two operations:

  • creating a new user
  • loading an existing user

Before we can provide an HTTP API, we’ll need to implement an actor that provides the operations we need:

class UserActor extends AbstractActor {

  private UserService userService = new UserService();

  static Props props() {
    return Props.create(UserActor.class);
  }

  @Override
  public Receive createReceive() {
    return receiveBuilder()
      .match(CreateUserMessage.class, handleCreateUser())
      .match(GetUserMessage.class, handleGetUser())
      .build();
  }

  private FI.UnitApply<CreateUserMessage> handleCreateUser() {
    return createUserMessage -> {
      userService.createUser(createUserMessage.getUser());
      sender()
        .tell(new ActionPerformed(
           String.format("User %s created.", createUserMessage.getUser().getName())), getSelf());
    };
  }

  private FI.UnitApply<GetUserMessage> handleGetUser() {
    return getUserMessage -> {
      sender().tell(userService.getUser(getUserMessage.getUserId()), getSelf());
    };
  }
}

Basically, we’re extending the AbstractActor class and implementing its createReceive() method.

Within createReceive(), we’re mapping incoming message types to methods that handle messages of the respective type.

The message types are simple serializable container classes with some fields that describe a certain operation. GetUserMessage and has a single field userId to identify the user to load. CreateUserMessage contains a User object with the user data we need to create a new user.

Later, we’ll see how to translate incoming HTTP requests into these messages.

Ultimately, we delegate all messages to a UserService instance, which provides the business logic necessary for managing persistent user objects.

Also, note the props() method. While the props() method isn’t necessary for extending AbstractActor, it will come in handy later when creating the ActorSystem.

For a more in-depth discussion about actors, have a look at our introduction to Akka Actors.

4. Defining HTTP Routes

Having an actor that does the actual work for us, all we have left to do is to provide an HTTP API that delegates incoming HTTP requests to our actor.

Akka uses the concept of routes to describe an HTTP API. For each operation, we need a route.

To create an HTTP server, we extend the framework class HttpApp and implement the routes method:

class UserServer extends HttpApp {

  private final ActorRef userActor;

  Timeout timeout = new Timeout(Duration.create(5, TimeUnit.SECONDS));

  UserServer(ActorRef userActor) {
    this.userActor = userActor;
  }

  @Override
  public Route routes() {
    return path("users", this::postUser)
      .orElse(path(segment("users").slash(longSegment()), id -> route(getUser(id))));
  }

  private Route getUser(Long id) {
    return get(() -> {
      CompletionStage<Optional<User>> user = 
        PatternsCS.ask(userActor, new GetUserMessage(id), timeout)
          .thenApply(obj -> (Optional<User>) obj);

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

  private Route postUser() {
    return route(post(() -> entity(Jackson.unmarshaller(User.class), user -> {
      CompletionStage<ActionPerformed> userCreated = 
        PatternsCS.ask(userActor, new CreateUserMessage(user), timeout)
          .thenApply(obj -> (ActionPerformed) obj);

      return onSuccess(() -> userCreated, performed -> {
        return complete(StatusCodes.CREATED, performed, Jackson.marshaller());
      });
    })));
  }
}

Now, there is a fair amount of boilerplate here, but note that we follow the same pattern as before of mapping operations, this time as routes. Let’s break it down a bit.

Within getUser(), we simply wrap the incoming user id in a message of type GetUserMessage and forward that message to our userActor.

Once the actor has processed the message, the onSuccess handler is called, in which we complete the HTTP request by sending a response with a certain HTTP status and a certain JSON body. We use the Jackson marshaller to serialize the answer given by the actor into a JSON string.

Within postUser(), we do things a little differently, since we’re expecting a JSON body in the HTTP request. We use the entity() method to map the incoming JSON body into a User object before wrapping it into a CreateUserMessage and passing it on to our actor. Again, we use Jackson to map between Java and JSON and vice versa.

Since HttpApp expects us to provide a single Route object, we combine both routes to a single one within the routes method. Here, we use the path directive to finally provide the URL path at which our API should be available.

We bind the route provided by postUser() to the path /users. If the incoming request is not a POST request, Akka will automatically go into the orElse branch and expect the path to be /users/ and the HTTP method to be GET.

If the HTTP method is GET, the request will be forwarded to the getUser() route. If the user does not exist, Akka will return HTTP status 404 (Not Found). If the method is nor a POST nor a GET, Akka will return HTTP status 405 (Method Not Allowed).

For more information about how to define HTTP routes with Akka, have a look at the Akka docs.

5. Starting the Server

Once we have created an HttpApp implementation like above, we can start up our HTTP server with a couple lines of code:

public static void main(String[] args) throws Exception {
  ActorSystem system = ActorSystem.create("userServer");
  ActorRef userActor = system.actorOf(UserActor.props(), "userActor");
  UserServer server = new UserServer(userActor);
  server.startServer("localhost", 8080, system);
}

We simply create an ActorSystem with a single actor of type UserActor and start the server on localhost.

6. Conclusion

In this article, we’ve learned about the basics of Akka HTTP with an example showing how to set up an HTTP server and expose endpoints to create and load resources, similar to a REST API.

As usual, the source code presented here can be found over on GitHub.