Overview

Swagger for Akka HTTP

No Comments

In this post I’m hoping to give you a good insight in how to setup Swagger for Akka HTTP.

Swagger is a tool to document REST API’s and overall works quite nicely to test some basic API commands. It already has integrations with many languages / REST frameworks. Since recently, Akka HTTP has been fully incorporated in the Akka platform. Swagger support for Akka HTTP exists, but the setup has some oddities. Here, I hope to provide you with a complete guideline.

Swagger Background

Swagger consists of two main parts; the Swagger UI and a server-side library. The server-side library is responsible for generating the API documentation from the code and supplemental annotations. This generated file is parsed by the Swagger UI, which in turn will provide an HTML app to view and test your API.

The generated API documentation looks something like this (in JSON):

{
  "tags": [{
    "name": "users"
  }],
  "host": "0.0.0.0:8080",
  "paths": {
    "/users": {
      "get": {
        "description": "",
        "tags": ["users"],
        "operationId": "getAllUsers",
        "produces": ["application/json"],
        "parameters": [],
        "summary": "Get list of all users",
        "responses": {
          "200": {
            "description": "successful operation",
            "schema": {
              "type": "array",
              "uniqueItems": true,
              "items": {
                "$ref": "#/definitions/User"
              }
            }
          }
        }
      }
    }
  },
  "basePath": "/",
  "info": {
    "description": "",
    "version": "1.0",
    "title": "",
    "termsOfService": ""
  },
  "schemes": ["http"],
  "definitions": {
    "Function1RequestContextFutureRouteResult": {
      "type": "object"
    },
    "User": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string"
        }
      }
    }
  },
  "swagger": "2.0"
}

As server-side library for generating the API documentation, we will use the swagger-akka-http project.

Project Setup and Dependencies

A new project may be generated with sbt-fresh, giving us already a nice project setup containing most of what we need. The layout should look like this;

+ build.sbt             // specific settings for (single) module
+ project
--+ build.properties    // sbt version
--+ Build.scala         // common settings for all modules
--+ Dependencies.scala  // values for library dependencies
--+ plugins.sbt         // sbt-git, sbt-header, sbt-scalariform
+ README.md
+ src
--+ main
----+ resources
----+ scala
------+ package.scala   // type aliases repoining `Seq` and friends to immutable

For the swagger-akka-http library (and for Akka itself), we need to add the following dependencies to the build.sbt file. Be nice and add these dependencies as variables to the project/Dependencies.scala file 😉

  "com.typesafe.akka" %% "akka-http-experimental" % 2.4.2
  "de.heikoseeberger" %% "akka-http-circe" % 1.5.2
  "io.circe" %% "circe-generic" % 0.3.0
  "io.circe" %% "circe-java8" % 0.3.0
  "com.github.swagger-akka-http" %% "swagger-akka-http" % 0.6.2

We would like to include the Swagger UI in our app – especially in this demo – because it lets us easily serve the necessary pages. For this, download the entire latest Swagger UI repository as ZIP from GitHub. Extract the archive and copy all files under the dist folder to our project’s src/main/resources/swagger/ folder.

Now we’re all set for some real code.

Akka HTTP Setup

For our demo application, the API itself is not really that interesting. Let’s assume we want to retrieve the users in our system, and add new users. This at least gives us a HTTP GET and POST request to describe and play with.

First we need an entry point for our application; the main class so to say. It will start our Actor system and from there run the HTTP service as Actor. The full code is here for UserApp.scala:

import akka.actor._
 
import scala.concurrent.Await
import scala.concurrent.duration.Duration
 
object UserApp {
  def main(args: Array[String]): Unit = {
    implicit val system = ActorSystem("myuser")
 
    system.actorOf(Props(new Master), "user-app-master")
 
    Await.ready(system.whenTerminated, Duration.Inf)
  }
}
 
class Master extends Actor with ActorLogging with ActorSettings {
  override val supervisorStrategy = SupervisorStrategy.stoppingStrategy
 
  private val userRepository = context.watch(createUserRepository())
  context.watch(createHttpService(userRepository))
 
  log.info("Up and running")
 
  override def receive = {
    case Terminated(actor) => onTerminated(actor)
  }
 
  protected def createUserRepository(): ActorRef = {
    context.actorOf(UserRepository.props(), UserRepository.Name)
  }
 
  protected def createHttpService(userRepositoryActor: ActorRef): ActorRef = {
    import settings.httpService._
    context.actorOf(
      HttpService.props(address, port, selfTimeout, userRepositoryActor),
      HttpService.Name
    )
  }
 
  protected def onTerminated(actor: ActorRef): Unit = {
    log.error("Terminating the system because {} terminated!", actor)
    context.system.terminate()
  }
}

As you can see in the code, it also initializes the UserRepository Actor. This Actor will function as a ‘stub’ for our persistent storage or other business logic. It doesn’t really matter for this demo application, just that we have some place where we can store and retrieve user records. Here is the code for UserRepository.scala:

import akka.actor.{ Props, ActorLogging }
 
object UserRepository {
 
  case class User(name: String)
  case object GetUsers
  case class AddUser(name: String)
  case class UserAdded(user: User)
  case class UserExists(name: String)
 
  final val Name = "user-repository"
  def props(): Props = Props(new UserRepository())
}
 
class UserRepository extends ActorLogging {
  import UserRepository._
 
  private var users = Set.empty[User]
 
  override def receive = {
    case GetUsers =>
      log.debug("received GetUsers command")
      sender() ! users
    case AddUser(name) if users.exists(_.name == name) =>
      sender() ! UserExists(name)
    case AddUser(name) =>
      log.info(s"Adding new user with name; $name")
      val user = User(name)
      users += user
      sender() ! UserAdded(user)
  }
}

And then finally the HTTP Service. I’m going to show you here a stripped-down version, to later show the full version with all annotations etc. necessary for Swagger.

object HttpService {
 
   // $COVERAGE-OFF$
  final val Name = "http-service"
  // $COVERAGE-ON$
 
  def props(address: String, port: Int, internalTimeout: Timeout, userRepository: ActorRef): Props =
    Props(new HttpService(address, port, internalTimeout, userRepository))
 
  private def route(httpService: ActorRef, address: String, port: Int, internalTimeout: Timeout,
    userRepository: ActorRef, system: ActorSystem)(implicit ec: ExecutionContext, mat: Materializer) = {
    import Directives._
    import io.circe.generic.auto._
 
    new UserService(userRepository, internalTimeout).route
  }
}
 
class HttpService(address: String, port: Int, internalTimeout: Timeout, userRepository: ActorRef)
    extends Actor with ActorLogging {
  import HttpService._
  import context.dispatcher
 
  private implicit val mat = ActorMaterializer()
 
  Http(context.system)
    .bindAndHandle(route(self, address, port, internalTimeout, userRepository, context.system), address, port)
    .pipeTo(self)
 
  override def receive = binding
 
  private def binding: Receive = {
    case serverBinding @ Http.ServerBinding(address) =>
      log.info("Listening on {}", address)
 
    case Status.Failure(cause) =>
      log.error(cause, s"Can't bind to $address:$port")
      context.stop(self)
  }
}
 
class UserService(userRepository: ActorRef, internalTimeout: Timeout)(implicit executionContext: ExecutionContext) extends Directives {
  import CirceSupport._
  import io.circe.generic.auto._
 
  implicit val timeout = internalTimeout
 
  val route = pathPrefix("users") { usersGetAll ~ userPost }
 
  def usersGetAll = get {
    complete {
      (userRepository ? UserRepository.GetUsers).mapTo[Set[UserRepository.User]]
    }
  }
 
  def userPost = post {
    entity(as[UserRepository.User]) { user =>
      onSuccess(userRepository ? UserRepository.AddUser(user.name)) {
        case UserRepository.UserAdded(_)  => complete(StatusCodes.Created)
        case UserRepository.UserExists(_) => complete(StatusCodes.Conflict)
      }
    }
  }
}

What is most important, is that you split up the HTTP Service and especially our UserService into separate classes. This will let us annotate the UserService class separately, only exposing this one for Swagger. Should you have multiple services, then splitting them up in the right way will also make sure the operations are nicely grouped.

Swagger Setup

Now finally to the part where we configure Swagger to generate the API documentation.

Serve Swagger UI Resources

In one of the first steps, we added all resources for the Swagger UI. To serve them, we need to tell Akka HTTP where and how to serve these files. Luckily this is very easy and requires only two lines in our HttpService class (remember, I’ll show the full class at the end):

def assets = pathPrefix("swagger") {
      getFromResourceDirectory("swagger") ~ pathSingleSlash(get(redirect("index.html", StatusCodes.PermanentRedirect))) }
 
assets ~ new UserService(userRepository, internalTimeout).route

The first line tells Akka HTTP where it can find the necessary files, and that we want them served on the path /swagger. The second line adds this route definition to the total routes definition.

Swagger Annotations

First of all on the service level, we need to use the @Api annotation. This will tell Swagger this is an entry point for a REST service, and the path it maps to. For example:

@Api(value = "/users", produces = "application/json")

The annotation for the GET request is also very easy. For this we use the @ApiOperation annotation. Most noteworthy here is the response and responseContainer attributes we have to supply. The response points to the class of the object that is being returned. It lets Swagger correctly show example responses (and later for the POST create example requests). The responseContainer is required when a list or set is returned, to describe the correct container type (and not to ‘pollute’ the object return type).

  @ApiOperation(value = "Get list of all users", nickname = "getAllUsers", httpMethod = "GET",
    response = classOf[UserRepository.User], responseContainer = "Set")

Lastly the annotation for the POST request. Again the @ApiOperation annotation is used, but now we also need the @ApiImplicitParams and @ApiResponses annotations. This last annotation is just to notify we may return other HTTP response codes besides 200 OK, and the reason why this response code might be returned.
The @ApiImplicitParams annotation describes the parameter supplied with the HTTP POST. Also here you need to point to a class so that Swagger can correctly describe the object. Strangely, where for the GET request @ApiOperation a class object was required, now the dataType attribute requires a string object.

  @ApiOperation(value = "Create new user", nickname = "userPost", httpMethod = "POST", produces = "text/plain")
  @ApiImplicitParams(Array(
    new ApiImplicitParam(name = "user", dataType = "UserRepository$User", paramType = "body", required = true)
  ))
  @ApiResponses(Array(
    new ApiResponse(code = 201, message = "User created"),
    new ApiResponse(code = 409, message = "User already exists")
  ))

For a full description of all Swagger annotations see https://github.com/swagger-api/swagger-core/wiki/Annotations-1.5.X

Last thing to note, apparently the javax.ws.rs.Path is always necessary for Swagger to detect the service. So while it has no use for Akka HTTP, you need to include it for Swagger.

Swagger HTTP Service

The SwaggerHttpService is a trait that needs to be included to actually generate and provide the JSON API documentation described above. Most importantly, you need to point it to your REST services, in our case the UserService. The full class is described below:

import com.github.swagger.akka.model.Info
import nl.codecentric.UserService
 
import scala.reflect.runtime.{ universe => ru }
import akka.actor.ActorSystem
import akka.stream.ActorMaterializer
import com.github.swagger.akka._
 
class SwaggerDocService(address: String, port: Int, system: ActorSystem) extends SwaggerHttpService with HasActorSystem {
  override implicit val actorSystem: ActorSystem = system
  override implicit val materializer: ActorMaterializer = ActorMaterializer()
  override val apiTypes = Seq(ru.typeOf[UserService])
  override val host = address + ":" + port
  override val info = Info(version = "1.0")
}

And then we need to include it in the Akka HTTP route definition:

    assets ~ new UserService(userRepository, internalTimeout).route ~ new SwaggerDocService(address, port, system).routes

CORS Support

One last thing we need is to configure HTTP cross-origin access control. To make sure the Swagger UI works with our service when running on a different domain or on localhost. The CorsSupport class I included in my project is taken directly from https://github.com/pjfanning/swagger-akka-http-sample. It is this class: https://github.com/pjfanning/swagger-akka-http-sample/blob/master/src/main/scala/com/example/akka/CorsSupport.scala.
Now just to hook it up into our project:

object HttpService extends CorsSupport {
 
...
 
    assets ~ corsHandler(new UserService(userRepository, internalTimeout).route) ~ corsHandler(new SwaggerDocService(address, port, system).routes)

Full HTTP Service Class

And finally the full HttpService class with the service, the annotations, CORS support etc.:

import javax.ws.rs.Path
 
import akka.actor._
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives
import akka.stream.{ Materializer, ActorMaterializer }
import akka.pattern.{ ask, pipe }
import akka.util.Timeout
import de.heikoseeberger.akkahttpcirce.CirceSupport
import nl.codecentric.user.swagger.SwaggerDocService
import nl.codecentric.user.util.CorsSupport
import scala.concurrent.ExecutionContext
import io.swagger.annotations._
 
object HttpService extends CorsSupport {
  // $COVERAGE-OFF$
  final val Name = "http-service"
  // $COVERAGE-ON$
 
  def props(address: String, port: Int, internalTimeout: Timeout, userRepository: ActorRef): Props =
    Props(new HttpService(address, port, internalTimeout, userRepository))
 
  private def route(httpService: ActorRef, address: String, port: Int, internalTimeout: Timeout,
    userRepository: ActorRef, system: ActorSystem)(implicit ec: ExecutionContext, mat: Materializer) = {
    import Directives._
    import io.circe.generic.auto._
 
    // format: OFF
    def assets = pathPrefix("swagger") {
      getFromResourceDirectory("swagger") ~ pathSingleSlash(get(redirect("index.html", StatusCodes.PermanentRedirect))) }
 
    assets ~ corsHandler(new UserService(userRepository, internalTimeout).route) ~ corsHandler(new SwaggerDocService(address, port, system).routes)
  }
}
 
class HttpService(address: String, port: Int, internalTimeout: Timeout, userRepository: ActorRef)
    extends Actor with ActorLogging {
  import HttpService._
  import context.dispatcher
 
  private implicit val mat = ActorMaterializer()
 
  Http(context.system)
    .bindAndHandle(route(self, address, port, internalTimeout, userRepository, context.system), address, port)
    .pipeTo(self)
 
  override def receive = binding
 
  private def binding: Receive = {
    case serverBinding @ Http.ServerBinding(address) =>
      log.info("Listening on {}", address)
 
    case Status.Failure(cause) =>
      log.error(cause, s"Can't bind to $address:$port")
      context.stop(self)
  }
}
 
@Path("/users")  // @Path annotation required for Swagger
@Api(value = "/users", produces = "application/json")
class UserService(userRepository: ActorRef, internalTimeout: Timeout)(implicit executionContext: ExecutionContext) extends Directives {
  import CirceSupport._
  import io.circe.generic.auto._
 
  implicit val timeout = internalTimeout
 
  val route = pathPrefix("users") { usersGetAll ~ userPost }
 
  @ApiOperation(value = "Get list of all users", nickname = "getAllUsers", httpMethod = "GET",
    response = classOf[UserRepository.User], responseContainer = "Set")
  def usersGetAll = get {
    complete {
      (userRepository ? UserRepository.GetUsers).mapTo[Set[UserRepository.User]]
    }
  }
 
  @ApiOperation(value = "Create new user", nickname = "userPost", httpMethod = "POST", produces = "text/plain")
  @ApiImplicitParams(Array(
    new ApiImplicitParam(name = "user", dataType = "nl.codecentric.UserRepository$User", paramType = "body", required = true)
  ))
  @ApiResponses(Array(
    new ApiResponse(code = 201, message = "User created"),
    new ApiResponse(code = 409, message = "User already exists")
  ))
  def userPost = post {
    entity(as[UserRepository.User]) { user =>
      onSuccess(userRepository ? UserRepository.AddUser(user.name)) {
        case UserRepository.UserAdded(_)  => complete(StatusCodes.Created)
        case UserRepository.UserExists(_) => complete(StatusCodes.Conflict)
      }
    }
  }
}

Disclaimer

The examples I showed you here are loosely based on Heiko Seeberger’s reactive-flows project regarding the Akka HTTP setup. A demo project can be easily generated with SBT-Fresh sbt-fresh.

Hope you liked this blog and would love to hear your comments!

Comment

Your email address will not be published. Required fields are marked *