Sunday, January 8, 2017

Building REST API web service using AKKA-HTTP, showing CRUD operations baked with Redis

Building REST API web service using AKKA-HTTP, showing CRUD operations baked with Redis

AKKA-HTTP is a lightweight layer that enable us to easily build our REST API over the akka actor system .
In this article I will show step by step how to create simple web server with REST API Service and CRUD operations using Rediscala that will enable non-blocking and asynchronous I/O operations over Redis, sowing different options of completing the API response and serializing entities . And of course how to test our REST API.

Full source code can be found here

Our web server will expose API for handling user passwords that will be kept in Redis (this is not a good practice to handle it like this in real life, and one might want to consider to add encryption etc’ in production).
Our Rest API will expose the following functionality :
  • Register new user
  • Update user password
  • Fetch user password
  • Delete user
So, let’s get our hands dirty

First step — add dependencies to our Build.sbt file

name := """akka-http-redis"""
version := "1.0"
scalaVersion := "2.11.8"
scalacOptions := Seq("-unchecked", "-deprecation", "-encoding", "utf8")
libraryDependencies ++= {
  val akkaV       = "2.4.3"
  val scalaTestV  = "2.2.6"
    "com.typesafe.akka" %% "akka-actor" % akkaV,
    "com.typesafe.akka" %% "akka-stream" % akkaV,
    "com.typesafe.akka" %% "akka-http-experimental" % akkaV,
    "com.typesafe.akka" %% "akka-http-spray-json-experimental" % akkaV,
    "com.typesafe.akka" %% "akka-http-testkit" % akkaV,
    "com.github.etaty" %% "rediscala" % "1.7.0",
    "org.scalatest"     %% "scalatest" % scalaTestV % "test",
     "org.scalamock" %% "scalamock-scalatest-support" % "3.4.2" % "test"

Next — Let’s write some code.

Now we are ready to build our server .
let’s start with simple trait for our db with the CRUD operations that we need :
trait RedisRepoImpl extends Repo {
  def db: RedisClient
  def del(key: String): Future[Long] = db.del(key)
  def upsert[V: ByteStringSerializer](key: String, value: V, expire: Option[Duration] = None): Future[Boolean] = db.set(key, value)
  def get(key: String): Future[Option[String]] = db.get[String](key)
*note since Redis “Set” command is used for inserting new key but if the key already exists — the value is overwritten, hence we will use it as “upsert” command.

UserHandler Actor

This actor will do the following:
  • Get the request from the Rest API layer
  • Communicate with the db
  • Do some “mambo jumbo” (AKA logic) and reply to the Rest API Layer
Note that we can directly send the response from a future to an actor using the pipeTo pattern. all you need is import akka.pattern.pipe and make sure to have ExecutionContext in scope. let’s see that in action (code):
object UserHandler {
  def props(db: Repo): Props = Props(new UserHandler(db))
  case class User(username: String, details: String)
  case class Register(username: String, password: String)
  case class Update(username: String, details: String)
  case class GetUser(username: String)
  case class DeleteUser(username: String)
  case class UserNotFound(username: String)
  case class UserDeleted(username: String)
//simple DI through constructor but of course you can use any DI //framework (e.g Guice, Spring ...) or DI pattern (e.g cake pattern) //but this is out of scope of this post 
class UserHandler(db: Repo) extends Actor with ActorLogging with {
  import UserHandler._
  implicit val ec = context.dispatcher
  override def receive: Receive = {
    case Register(id, pwd) =>
      db.upsert(id, pwd) pipeTo sender()
    case Update(id, details) =>
      db.upsert(id, details) pipeTo sender()
case GetUser(userName) =>
      //closing over the sender in Future is not safe.
      val requestor = sender()
        case Some(i) => requestor ! User(userName, i)
        case None => requestor ! UserNotFound
case DeleteUser(userName) =>
      val requestor = sender()
        case effectedRows if effectedRows > 0 => 
                         requestor ! UserDeleted(userName)
        case _ => requestor ! UserNotFound(userName)
Now we are finally ready for

Writing REST API using AKKA-HTTP

Writing REST API with akka-http is simple, all we need to do is to create Routes i.e add the paths that we want, and unmarshal the request body. in order to achieve that I’ll create some case classes that will represent the request body and I’ll use spray-json to de/serialize json into a case class.
case class UserPwd(pwd:String)
case class UpsertRequest(username:String, password:String )
trait Protocols extends DefaultJsonProtocol {
  implicit val delUserFormat = jsonFormat1(UserDeleted.apply)
  implicit val uNotFoundFormat = jsonFormat1(UserNotFound.apply)
  implicit val usrFormat = jsonFormat2(User.apply)
  implicit val userLogin = jsonFormat2(UpsertRequest.apply)

Serializing the request

let’s start with the registration. In this case registering new user will not require authentication and will comply to the following PUT /api/user/register . Note the ease that we can deserialize the request body to a case class. The code is quite self explanatory :
val unsecuredRoutes: Route = {
    pathPrefix("api/user") {
      path("register") {
          put {
            entity(as[UpsertRequest]) { u =>
              complete {
                (userHandler ? UserHandler.Register(u.username, u.password)).map {
                  case true => OK -> s"Thank you ${u.username}" //plain text response
                  case _ => InternalServerError -> "Failed to complete your request. please try later"


The other operations (delete user, update password) will require authentication. We will use simple basic user/pwd authentication (note that this implementation is quite stupid because every authenticated user will be able to execute this operations on every user but it will do just for the demo)
so we need to create the functionality the will authenticate the requestor e.g
def userAuthenticate(credentials: Credentials): Future[Option[User]]
now we can use Akka basic authentication support to wrap the inner route.

A word regarding the path matching

Akka-HTTP have a rich DSL for path matching, it is very powerful and convenient to use. for example note the use of the ~ sign to chain the inner routes having the same prefix (“/user”) and the “Segment” keyword that enable us to capture text following the “/user/” path . for example
using this get request
curl -i -X GET \
with this PathDirectives
path(Segment) { id =>
will map value (123) captured by the Segment keyword to the “id” variable

Responding to requestor

Since we defined the serialization protocol our case class response will be converted to JSON response,
we can map the “Future” that we got from the handler and map it to our case class using mapTo. in the following example I will show two types of responses JSON and Plain Text.
* This is not a good practice to do in production. Make your API consist on single response type (e.g JSON) .
val routes: Route =
    logRequestResult("akka-http-secured-service") {
      authenticateBasicAsync(realm = "secure site",
                                    userAuthenticate) { user =>
        pathPrefix("user") {
          path(Segment) { id =>
              get {
                complete {
                  //Response with JSON
                  (userHandler ? GetUser(id)).mapTo[User]
            } ~
            path(Segment) { id =>
              post {
                entity(as[UpsertRequest]) { u =>
                  complete {
                    //Response with Plain Text
                    (userHandler ?UserHandler.Update(u.username,
                                                  u.password)).map {
                      case false => 
                       InternalServerError -> s"Could not update  
                                                           user $id"
                      case _ => 
                            NoContent -> ""
The rest of the code can be found here

The Server — Main

Now we need to bind our routes to http host and port
object AkkaHttpRedisService extends App with Service {
  override implicit val system = ActorSystem()
  override implicit val executor = system.dispatcher
  override implicit val materializer = ActorMaterializer()
  override val config = ConfigFactory.load()
  override val logger = Logging(system, getClass)
  val userHandler = system.actorOf(UserHandler.props)
  Http().bindAndHandle(unsecuredRoutes ~ routes , config.getString("http.interface"), config.getInt("http.port"))


Everything should be tested. But in order not to make this post too long we will focus on testing the REST Api.
Since we want to maintain the purity of our unit tests we can mock our db. for that we will use scalamock and mixin the “MockFactory” (
Akka supplies ScalatestRouteTest trait that we can mixin with our test framework and to test any aspect of the response for example :
class ServiceSpec extends FlatSpec
  with Matchers
  with ScalatestRouteTest
  with Service
  with MockFactory {
  val repo: Repo = stub[Repo]
  val userHandler = TestActorRef[UserHandler](new UserHandler(repo))
  "Registration Service" should "add user" in {
    val userName = "newuser"
    val pwd = "123pwd"
    //mock the response from the db
    (repo.upsert[String](_: String, _: String, _: Option[Duration])(_: ByteStringSerializer[String]))
      .when(userName, pwd, None, *) returns Future.successful(true)
//route call  
    Put(s"/api/user/register", UpsertRequest(userName, pwd)) ~>
      unsecuredRoutes ~> check {
      status shouldBe OK
      responseAs[String] shouldBe s"Thank you $userName"
  "Secured service" should "get user" in {
    repo.get _ when validUser returns Future.successful(Some(pwd))
    Get(s"/user/$validUser") ~> addCredentials(userCredentials) ~>
      routes ~> check {
      status shouldBe OK
      contentType shouldBe `application/json`
      responseAs[User] shouldBe User(validUser,pwd)
That’s it !
Full source code can be found here
Hope you enjoyed. Your comments and inputs are greatly appreciated .
You are welcome to contribute to the code

No comments:

Post a Comment