Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

An Opinionated Guide to Building APIs With Akka-Http

DZone's Guide to

An Opinionated Guide to Building APIs With Akka-Http

APIs are a cornerstone of integration. Read on to learn how to create them with the Akka-Http framework, and get to building!

· Integration Zone ·
Free Resource

SnapLogic is the leading self-service enterprise-grade integration platform. Download the 2018 GartnerMagic Quadrant for Enterprise iPaaS or play around on the platform, risk free, for 30 days.

Akka-Http is my preferred framework for building APIs, but there are some things I have picked up along the way. For one thing, Akka-Http is very un-opinionated in its approach, there are often lots of ways to do the same thing, and there isn't a lot of opinionated guidance about how to do things.

I have been writing Akka-Http APIs for, I guess, about 18 months now (not long, I know), having previously worked predominantly with libraries like Spring, and I have seen some pretty nasty code resulting from this (by this I mean, I have written nasty code - not intentionally, of course, but from good intentions starting off trying to write, clean, idiomatic Akka-Http code, and ending up in huge sprawling routing classes which are un-readable and generally not very nice).

The routing DSL is Akka-Http, which is pretty nice but can quickly become unwieldy. For example, let's imagine you start off with something like this:

val route =
  pathPrefix(“user” / JavaUuid) { userId =>
    pathSingleSlash {
      get {
        complete {
          //fetch user details
        }
      }
    }
  }

This looks nice, right? A simple nested approach to the routing structure that reflects the URL hierarchy and the HTTP method, etc. However, as you can probably imagine, try and scale this up to a full application it can very easily become fairly messy. The nested directives make it nice to group routes under similar root URLs but as you do that you end up with very long, arrow-shaped code that actually isn't that easy to follow - if you have several endpoints nested within the structure it actually becomes quite hard to work out what endpoints there are and what is handling what.

Another problem that needs to be managed is that with the first one or two endpoints you might put the handling code directly in the routing structure, which is ok for very small numbers, but it needs to be managed sensibly as the endpoints grow and your routing structure starts to look more and more sprawling.

It is, of course, personal preference, but even with the simple example above, I don't like the level of nesting that already exists there to simply define the mapping of the GET HTTP method and a given URL - and if you add more endpoints and start to break down the URL with additional directives per URL section then the nesting increases.

To simplify the code, and keep it clean from the start I go for the following approach:

  1. Make sure your Routing classes are sensibly separated - probably by the URL root (e.g. have a single UserRoutes class that handles all URLs under /users) to avoid them growing too much.
  2. Handoff all business logic (well, within reason) to a service class - I use Scala's Self-Type notation to handle this and keep it nicely de-coupled
  3. Use custom directives and non-nested routings to make the DSL more concise.
Most of these steps are simple and self-explanatory, so its probably just step 3 that needs some more explanation. To start with, here is a simple example:
class UserRoutes {
  this: UserServiceComponent =>

  val routes: Route =
    getPath("users" / JavaUUID) { uuid =>
      respond(userService.getUser(GetUserRequest(None, Some(uuid))))
    } ~
    getPath("users" / Segment) { handle =>
      respond(userService.getUser(GetUserRequest(Some(handle), None)))
    }

}

You can see points 1 and 2 simply enough, but you will also notice that my endpoints are simple functions, without multiple levels of nesting (we may need some additional nesting at some point, as some endpoints will likely need other akka-http directives, but we can strive to keep it minimal).

You might notice I have duplicated the URL section "users" rather than nesting it - some people might not like this duplication (and I guess risk of error/divergence of URLs - but that can be mitigated with having predefined constants instead of explicit strings), but I prefer the readability and simplicity of this over extensive nesting.

Custom Directives

First off, I have simply combined a couple of existing directives to make it more concise. Normally, you might have several levels of nested directives such as one or more pathPrefix(“path”) sections, the HTTP Method such as get{}, another one to match pathEndOrSingleslash{}, etc. To avoid this, I have concatenated some of these to convenient single points.

trait MethodAndPathDirectives {
  def getPath[L](x: PathMatcher[L]): Directive[L] = get & path(x)
  def postPath[L](x: PathMatcher[L]): Directive[L] = post & path(x)
  def putPath[L](x: PathMatcher[L]): Directive[L] = put & path(x)
  def deletePath[L](x: PathMatcher[L]): Directive[L] = delete & path(x)
  def headPath[L](x: PathMatcher[L]): Directive[L] = head & path(x)
}

getPath, postPath, putPath, etc., simply combine the HTTP method with the URL path-matcher and also includes the existing Akka-Http directive “redirectToTrailingSlashIfMissing” which avoids having to specify matching on either a slash or path end and instead allows you to always match exact paths. It basically squashes the three directives in the original HelloWorld example above down to one simple, readable directive.

Custom Serialization

You may also notice, I have implemented a custom method called “respond” - I use this to handle the serialization of the response to a common JSON shape and to handle errors. Using this approach, I define a custom Response wrapper type that is essentially an Either of our internal custom error type and a valid response type T (implementation details below) - this means in all our code we have a consistent type that can be used to handle errors and ensure consistent responses.

This respond method simply expects a Response type to be passed to it (along with an optional success status code - defaulting to 200 OK, but can be provided to support alternative success codes). The method then uses Circe and Shapeless to convert the Response to a common JSON object. 

Let's have a look at some of the details, first the custom types I have defined for errors and custom Response type:

type Response[T] = Either[AkkOpError, T]

object Errors {

  sealed trait AkkOpError {
    val statusCode: Int
    val data: String
  }

  case class NotFound(data: String, statusCode: Int = StatusCodes.NotFound.intValue) extends AkkOpError
  case class BadRequest(data: String, statusCode: Int = StatusCodes.BadRequest.intValue) extends AkkOpError

}

Simple, now let's take a look at the implementation of the respond method:

trait ResponseHandler {
  this: ResponseWrapperEncoder =>
  import io.circe.syntax._
  import io.circe.generic.auto._

  def respond[A](response: Future[Response[A]], successStatusCode: StatusCode = StatusCodes.OK)
    (implicit ee: Encoder[AkkOpError], te: Encoder[A]): StandardRoute = {
    complete(responseTuple(response))
  }

  private[routing] def responseTuple[A](response: Future[Response[A]], successStatusCode: StatusCode = StatusCodes.OK)
    (implicit ee: Encoder[AkkOpError], te: Encoder[A])= {
    response map {
      case Left(er) =>
        (StatusCode.int2StatusCode(er.statusCode),
          wrap(StatusCode.int2StatusCode(er.statusCode), er.asJson).toString)
      case Right(a) =>
        (successStatusCode,
          wrap(successStatusCode, a.asJson).toString)
    }
  }
}

trait ResponseWrapperEncoder {
  def wrap(status: StatusCode, data: Json, metaData: Option[Json] = None): Json
}

It might look daunting (or not, depending on your familiarity with Scala and Shapeless), but its relatively simple. The two implicit Encoder arguments that are included on the method signature simply ensure that whatever type A is in the provided Response[A], Circe and shapeless are able to serialize it. If you try to pass some response to this method that can't be serialized, you get a compile error. After that, all it does is wrap the response A in a common message and return that along with an appropriate (or provided) HTTP status code.

You might also notice the final result is built using the wrap method in the ResponseWrapperEncoder trait - this allows easy extension/overriding of what the common response message looks like.

Conclusion

All of this machinery is, of course, abstracted away to a common library that can be used across different projects, and so, in reality, it means we have a consistent, clean API with simple routing classes as simple and neat as below, whilst also handing off our business logic to neater, testable services.

All the code for my opinionated library and an example API is all on GitHub, and it is currently in progress with more ideas underway!

class UserRoutes {
  this: UserServiceComponent =>

  val routes: Route =
    getPath("users" / JavaUUID) { uuid =>
      respond(userService.getUser(GetUserRequest(None, Some(uuid))))
    } ~
    getPath("users" / Segment) { handle =>
      respond(userService.getUser(GetUserRequest(Some(handle), None)))
    }

}

With SnapLogic’s integration platform you can save millions of dollars, increase integrator productivity by 5X, and reduce integration time to value by 90%. Sign up for our risk-free 30-day trial!

Topics:
integration ,apis ,api development

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}