Simple Web API with Akka-http and Redis Database

Background:

On a recent exploration to understand the inner working of Akka-http especially when building scalable REST API, this took me to dig a little into the intuition of how to do just this with the most expressive programming language there is, Scala.

Why Scala:

Scala is a language I have admired for a while, but the learning curve could be pretty steep. But once you get a hang of it, it’s a language you will love.

The Start — About Akka-http

Akka-http is a complete module to build full-server and client-side HTTP, that’s solely what it is, it’s important to note that Akka-http is not a web framework like Play, this is infact clearly emphasized on Akka-http site. Akka-http is well documented but understanding it’s whole concept can be a little tricky.

This article is basically to portray a layman understanding of what I found out exploring Akka-Http.

Akka-htttp implements Akka Actor model and Akka-stream backend. This enables Akka-http to be capable of implementing a good message driven and asynchronous architecture.

Actor System

Building Akka-http:

First, you need to create an sbt project, usually, there are seed project you can start with, Akka-http has it’s own seed project, however I will prefer a simple Scala project for this.

$ sbt new scala/scala-seed.g8

Once the project has been created we will need to update the build.sbt to include the necessary packages. Here is my sample sbt file.

lazy val akkaHttpVersion = "10.1.3"
lazy val akkaVersion = "2.5.14"
lazy val Json4sVersion = "3.5.2"
lazy val root = (project in file(".")).
settings(
inThisBuild(List(
organization := "com.akkaserver",
scalaVersion := "2.12.6"
)),
name := "akkaserver",
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-http" % akkaHttpVersion,
"com.typesafe.akka" %% "akka-http-spray-json" % akkaHttpVersion,
"com.typesafe.akka" %% "akka-stream" % akkaVersion,
      "com.typesafe.akka" %% "akka-http-testkit"    % akkaHttpVersion % Test,
"com.typesafe.akka" %% "akka-testkit" % akkaVersion % Test,
"com.typesafe.akka" %% "akka-stream-testkit" % akkaVersion % Test,
"org.scalatest" %% "scalatest" % "3.0.5" % Test,
"com.typesafe.akka" %% "akka-slf4j" % akkaVersion,
"de.heikoseeberger" %% "akka-http-json4s" % "1.16.0",
"net.debasishg" %% "redisclient" % "3.7",
"org.scala-lang" % "scala-actors" % "2.11.9"
)
)

Importance of Some of the key libraries

  1. akka-http — This is the key library that help implement most of akka-http functionalities
  2. akka-http-spray-json — This is the library that helps to implement the json support for Akka-http. This is particularly important when you are marshalling a json object request to Scala’s Case Class. The importance of this will be discussed in later part of the article.
  3. redisclient — This is the client that allow us to connect to Redis Database such that our POST can be stored and we can also run a GET method against our Server.

Once this is done, we can go ahead and create a Case Class to model our requests, a simple example will be a case class that only takes an ID and corresponding Value both of which are strings.

case class SimpleObject(id:String, value:String)

Usually, requests are sent in different representations such as Json or Xml, therefore representing incoming request as case class allow scala to easily receive the request and manipulate the information effectively. This concept of turning a request content type to Case Class is termed Marshalling.

This will then lead us to how to prepare a case class to be implicitly marshalled in akka-http. Implementing the below trait helps to implicitly convert our case class to a json serializable format. And when we do a POST request, we can as well marshall a json content-type to our Case Class.

trait ObjectJsonSupport extends DefaultJsonProtocol with SprayJsonSupport {
implicit val SomeObjectFormarts = jsonFormat2(Image)
}
object ObjectJsonSupport extends ObjectJsonSupport

It’s important to note that there are different variations of jsonformat supported by Spray JSON package and this depends often on the number of elements in your case class. Some varieties include:

  • jsonformat2
  • jsonformat3
  • jsonformat4 etc.

Once we have this, we can now implements our services.

With Akka-http, it is common to specify headers for your response. Here we look at an awesome approach encouraged by akka-http, it’s to respond with a request header. For this akka-http has a respondWithHeaders method.

Here is our implementation that takes care of several request format.

def completeWithLocationHeader[T](resourceId: Future[Option[T]], ifDefinedStatus: Int, ifEmptyStatus: Int): Route =
onSuccess(resourceId) {
case Some(t) => completeWithLocationHeader(ifDefinedStatus, t)
case None => complete(ifEmptyStatus, None)
}

def completeWithLocationHeader[T](status: Int, resourceId: T): Route =
extractRequestContext { requestContext =>
val request = requestContext.request
val location = request.uri.copy(path = request.uri.path / resourceId.toString)
respondWithHeader(Location(location)) {
complete(status, None)
}
}

The above will help take care a POST Response to ensure that the response is completed with the necessary header and also ensure to POST in a uri location that includes the key of the incoming Content.

def complete[T: ToResponseMarshaller](resource: Future[Option[T]]): Route =
onSuccess(resource) {
case Some(t) => complete(ToResponseMarshallable(t))
case None => complete(404, None)
}

def complete(resource: Future[Unit]): Route = onSuccess(resource) { complete(204, None) }
}

The above takes care of both success and bad response cases for GET. Usually, during the GET request, there is a scala code involved and there is every likelihood that what you get is an Instance of a Class or an Object, this is where the marshalling comes into play, since you have a marshalling on your case class, for the response, hence you can basically return an instance of the Case class which is of type that can undergo Marshalling hence the reason our complete method takes element of type T: ToResponseMarshaller and once we have a response we can then Convert this with ToResponseMarshallable this basically converts our GET Request to a JSON format which is returned to the client.

Before we finally declare our route and put everything together, it is important to look at the Redis database implementation.

Redis is a NOSQL In-memory Database that is sufficiently scalable. For our project, the basic commands we need with Redis is to set and get. If you need to get familiar with some of the commands of Redis, please visit this link.

Implementing the get and set method for our service is quite easy, here is what it looks like:

val client = new RedisClient("localhost", 6379)

def
set(objectValue: SimpleObject): Future[Option[String]] = Future {
get(objectValue.id) match {
case Some(i) => None
case _ => client.set(objectValue.id, objectValue.value)
}
Some(objectValue.id)
}

def get(id: String): Option[String] = client.get(id)

/***
* Helper function to get data from Redis Database
*
@param id
* @return
*/
def
getEntity(id: String) :Future[Option[Image]] = Future {
Some(SimpleObject(id, get(id)))
}
}

Once, a Redis Database has been started, you can connect a client fairly easily by creating an instance of RedisClient with the address and port on which your database is running.

The client basically comes with a set and get entity, which you may need to recompose into a case class for your response to effectively marshal to a json response.

With this, we can then proceed to implement our routes.

val redisModelService : RedisService

def someRoutes: Route = pathPrefix("sampleroute") {
pathEnd {
post {
entity(as[SimpleObject]) { s =>
completeWithLocationHeader(
resourceId = redisModelService.set(s),
ifDefinedStatus = 201, ifEmptyStatus = 409
)
}
}
} ~
path(Segment) { id =>
println(id)
get {
complete(redisModelService.getEntity(id))
}
}
}
}

Akka-http provides this very high-level approach to specify your request and the activities going on in there.

For the various routes, our POST method sends a request to a path -address:port/{pathprefix}. The essence of the entity(as[SimpleObject]) is to Unmarshall the incoming requests to the case class before sending forward to the route. Once this is completed, the values are then passed into completeWithHeader method for redisServiceto persist the data into DB. VIOLA! that’s our POST request interacting with a DB.

And similar stuff goes for the GET method, since we already implemented a method to return the result marshalled to JSON. With this we can basically instantiate the context that takes care of initiating the api requests and pass it accordingly to the resources.

implicit val system = ActorSystem("model-server")
implicit val materializer = ActorMaterializer()
implicit val executionContext = system.dispatcher
implicit val timeout = Timeout(10 seconds)

val api = routes

Http().bindAndHandle(handler = api, "localhost", 8080) map {
binding => println(s"REST interface bound to ${binding.localAddress}")
} recover { case ex =>
println(s"REST interface could not bind to localhost", ex.getMessage)

One thing to note is how we passed the execution context to the Service, it was implicitly declared in the RedisServer such that whenever that service is called, the current execution context is used to perform the request.

So, that’s it about building a REST API with akka-http I hope some people will find this useful in their sojourn in building scalable and Resilient back-end Service with Scala.

You can get the complete code on Github and play around with it. I hope to expand on this to create a full-fledged application that will be hosted, please watch out for more.

You can obtain even more details in this link or the official Akka-http site. Thanks to Richard who helped debug a critical bug with the use of Implicit Execution Context.

I will love to read any response and comments as regards these article.