Building RESTful APIs with Scala using Spray

Overview

There are many ways to build RESTful APIs with Scala and Spray (spray.io). Here we will review how to create end points which expose functionality and also how to call other RESTful web services taking advantages of APIs you or someone else may have written. We will use Google’s Elevations and Timezone API to demonstrate the later functionality.

Libraries Used

Setting Up the Project

The first thing we need to do is create the directory structure and add some of the other plumbing so that we can get to the real work. First you will want to create the project directory. I created a directory named SprayApiDemo in the place where I keep all of my other projects. This is the directory where we will want to create the following directory structure:

├── SprayApiDemo
│   ├── project
│   │   build.properties
│   │   plugins.sbt
│   └── src
│       ├── main
│       │   ├── resources
│       │   └── scala
│       └── test
│           └── scala
└── build.sbt

Here is the code required to create a compiling scala projects that brings in the proper dependencies for the libraries we will be using:

build.properties

sbt.version=0.13.0

plugins.sbt

addSbtPlugin("io.spray" % "sbt-revolver" % "0.7.1")
addSbtPlugin("org.ensime" % "ensime-sbt-cmd" % "0.1.2")

build.sbt

name := "SprayApiDemo"

version := "0.1"

scalaVersion := "2.10.2"

scalacOptions := Seq("-unchecked", "-deprecation", "-encoding", "utf8")

resolvers ++= Seq(
  "spray repo" at "http://repo.spray.io/"
)

libraryDependencies ++= {
  val sprayVersion = "1.2-M8"
  val akkaVersion = "2.2.0-RC1"
  Seq(
  "io.spray" % "spray-can" % sprayVersion,
  "io.spray" % "spray-routing" % sprayVersion,
  "io.spray" % "spray-testkit" % sprayVersion,
  "io.spray" % "spray-client" % sprayVersion,
  "io.spray" %%  "spray-json" % "1.2.5",
  "com.typesafe.akka" %% "akka-actor" % akkaVersion,
  "com.typesafe.akka" %% "akka-slf4j" % akkaVersion,
  "com.typesafe.akka" %% "akka-testkit" % akkaVersion % "test",
  "ch.qos.logback" % "logback-classic" % "1.0.12",
  "org.scalatest" %% "scalatest" % "2.0.M7" % "test"
  )
}

seq(Revolver.settings: _*)

You should now be able to compile the project which won’t do much since we haven’t written anything yet. Compiling what we have so far will help confirm our build files were written properly and sbt is working as expected. You may do this by executing `sbt` while in the root directory of your project. When this is done you should see the specified dependencies being installed if they have not been already.

Creating Our Tests

Now that we have created a skeleton project, we can start adding functionality. But before we get too ahead of ourselves, let’s write a few tests. This will help us think about what we want our application to do. We will use the FreeSpec trait provided to us by the ScalaTest library. Let’s begin by adding two files to the `scala` directory in the `src\test` folder; ElevationServiceSpec.scala and TimezoneServiceSpec.scala. Each test will confirm that when the our ElevationService or TimezoneService API is called we receive the appropriate response. Obviously these tests will fail until we write the services.

ElevationServiceSpec.scala

package com.christophergagne.sprayapidemo

import scala.concurrent.future
import org.scalatest.BeforeAndAfterAll
import org.scalatest.FreeSpec
import org.scalatest.Matchers
import spray.http.StatusCodes._
import spray.testkit.ScalatestRouteTest

class ElevationServiceSpec extends FreeSpec with SprayApiDemoService with ScalatestRouteTest with Matchers {
  def actorRefFactory = system

  "The Elevation Service" - {
    "when calling GET api/ElevationService/39/80" - {
      "should return '1159.288940429688'" in {
        Get("/api/ElevationService/39/80") ~> sprayApiDemoRoute ~> check {
          status should equal(OK)
          entity.toString should include("1159.288940429688")
        }
      }
    }
  }
}

TimezoneServiceSpec.scala

package com.christophergagne.sprayapidemo

import scala.concurrent.future
import org.scalatest.BeforeAndAfterAll
import org.scalatest.FreeSpec
import org.scalatest.Matchers
import spray.http.StatusCodes._
import spray.testkit.ScalatestRouteTest

class TimezoneServiceSpec extends FreeSpec with SprayApiDemoService with ScalatestRouteTest with Matchers {
  def actorRefFactory = system

  "The Timezone Service" - {
    "when calling GET /api/TimezoneService/39/-119/1331161200" - {
      "should return 'Pacific Standard Time'" in {
        Get("/api/TimezoneService/39/-119/1331161200") ~> sprayApiDemoRoute ~> check {
          status should equal(OK)
          entity.toString should include("Pacific Standard Time")
        }
      }
    }
  }
}

Defining the Services and Routes

The next step is to create our service class and define our routes. We will create our service in a way where we can easily test each end point we want to expose. In the `scala` directory inside the `src/main` folder we will create the SprayApiDemoService.scala file. This will be where we define our parent akka actor and the service trait. Creating the service as a trait gives us the option to mix-in our service into other our parent akka actor or a test. This is a small change, with a lot of benefits.

Please also observe our routings. You can see that each service endpoint has a path prefix of `api`. This means just that, both endpoints will be found under `api`, as in http://xyz.com/api/… We are defining two endpoint which take varying parameters; ElevationService and TimezoneService. The first requires two double values to be passed, as in http://xyz.com/api/ElevationService/39/80. The seconds requires two double values and an additional segment parameter to be passed, as in http://xyz.com/api/TimezoneService/39/-119/1331161200.

You may notice that our application doesn’t compile yet. This is because we are missing the pieces that will do the real work (ElevationService and TimezoneService). Don’t worry, we’ll get to building those pieces later, which is also when you will see how we can consume someone else’s API.

SprayApiDemoService.scala

package com.christophergagne.sprayapidemo

import akka.actor.{Actor, Props}
import akka.event.Logging
import spray.routing._
import spray.http._
import MediaTypes._

class SprayApiDemoServiceActor extends Actor with SprayApiDemoService {
  
  def actorRefFactory = context

  def receive = runRoute(sprayApiDemoRoute)
}

trait SprayApiDemoService extends HttpService {
  val sprayApiDemoRoute =
    pathPrefix("api") {
      path("ElevationService" / DoubleNumber / DoubleNumber) { (long, lat) =>
        requestContext =>
          val elevationService = actorRefFactory.actorOf(Props(new ElevationService(requestContext)))
          elevationService ! ElevationService.Process(long, lat)
      } ~
      path("TimezoneService" / DoubleNumber / DoubleNumber / Segment) { (long, lat, timestamp) =>
        requestContext =>  
          val timezoneService = actorRefFactory.actorOf(Props(new TimezoneService(requestContext)))
          timezoneService ! TimezoneService.Process(long, lat, timestamp)
      }
    }
}

Bootstrapping the Service

Let’s bootstrap our application by creating the class which has the main method. We will call this `Boot.scala` and we will put it in the same directory as our service class. This class will also initialize our Akka actor system and create, initialize logging, create an instance of our service and start our HTTP server.

Boot.scala

package com.christophergagne.sprayapidemo

import akka.actor.{ActorSystem, Props}
import akka.event.Logging
import akka.io.IO
import spray.can.Http

object Boot extends App {

  // we need an ActorSystem to host our application in
  implicit val system = ActorSystem("spray-api-service")
  val log = Logging(system, getClass)

  // create and start our service actor
  val service = system.actorOf(Props[SprayApiDemoServiceActor], "spray-service")

  // start a new HTTP server on port 8080 with our service actor as the handler
  IO(Http) ! Http.Bind(service, interface = "localhost", port = 8080)
}

Creating the ElevationService

Let’s now build something that will do something. The elevation service will actually take advantage of Google’s Elevation service, which you may want to take a quick look at. Focus on the JSON response that will look something like this:

{
  results: [{
    elevation: 4838.74072265625,
    location: {
      lat: 30,
      lng: 100
    },
    resolution: 152.7032318115234
  }],
  status: "OK"
}

With this knowledge, we can now create the class which will help us deserialize the results from Google. For this you will want to create a the Elevation.scala file. Here we will use spray-json to serialize the JSON response. The classes and objects in this source file will be used when we create the Elevation Service next.

Elevation.scala

package com.christophergagne.sprayapidemo

import spray.json.{ JsonFormat, DefaultJsonProtocol }

case class Elevation(location: Location, elevation: Double)
case class Location(lat: Double, lng: Double)
case class GoogleElevationApiResult[T](status: String, results: List[T])

object ElevationJsonProtocol extends DefaultJsonProtocol {
  implicit val locationFormat = jsonFormat2(Location)
  implicit val elevationFormat = jsonFormat2(Elevation)
  implicit def googleElevationApiResultFormat[T :JsonFormat] = jsonFormat2(GoogleElevationApiResult.apply[T])
}

Now create the ElevationService.scala source file. This file will define our ElevationService class, which will be a Akka Actor and it’s companion object. When the ElevationService receives a message, it will then take the two Double parameters passed to it and use Spray’s client API to send a message to Google’s API, receive a response, serialize the response to our Elevation object and return the elevation value. If for some reason the request to Google fails, we will send back a failure as well.

ElevationService.scala

package com.christophergagne.sprayapidemo

import akka.actor.{Actor, ActorRef}
import akka.event.Logging
import akka.io.IO

import spray.routing.RequestContext
import spray.httpx.SprayJsonSupport
import spray.client.pipelining._

import scala.util.{ Success, Failure }

object ElevationService {
  case class Process(long: Double, lat: Double)
}

class ElevationService(requestContext: RequestContext) extends Actor {

  import ElevationService._

  implicit val system = context.system
  import system.dispatcher
  val log = Logging(system, getClass)

  def receive = {
    case Process(long,lat) =>
      process(long,lat)
      context.stop(self)
  }

  def process(long: Double, lat: Double) = { 

    log.info("Requesting elevation long: {}, lat: {}", long, lat)

    import ElevationJsonProtocol._
    import SprayJsonSupport._
    val pipeline = sendReceive ~> unmarshal[GoogleElevationApiResult[Elevation]]

    val responseFuture = pipeline{
      Get(s"http://maps.googleapis.com/maps/api/elevation/json?locations=$long,$lat&sensor=false")
    }
    responseFuture onComplete {
      case Success(GoogleElevationApiResult(_, Elevation(_, elevation) :: _)) =>
        log.info("The elevation is: {} m", elevation)
        requestContext.complete(elevation.toString)

      case Failure(error) =>
        requestContext.complete(error)
    }
  }
}

Creating the TimezoneService

The final step to creating our service is to write the TimezoneService. This process is very similar to creating the ElevationService, however this service will demonstrate that we’re able to send requests to APIs using https (a secure connection).

Timezone.scala

package com.christophergagne.sprayapidemo

import spray.json.{ JsonFormat, DefaultJsonProtocol }

case class Timezone(status: String, timeZoneId: String, timeZoneName: String)
case class GoogleTimezoneApiResult[T](status: String, timeZoneId: String, timeZoneName: String)

object TimezoneJsonProtocol extends DefaultJsonProtocol {
  implicit val timezoneFormat = jsonFormat3(Timezone) 
  implicit def googleTimezoneApiResultFormat[T :JsonFormat] = jsonFormat3(GoogleTimezoneApiResult.apply[T])
}

TimezoneService.scala

package com.christophergagne.sprayapidemo

import akka.actor.{Actor, ActorRef}
import akka.event.Logging
import akka.io.IO

import spray.routing.RequestContext
import spray.httpx.SprayJsonSupport
import spray.client.pipelining._

import scala.util.{ Success, Failure }

object TimezoneService {
  case class Process(long: Double, lat: Double, timestamp: String)
}

class TimezoneService(requestContext: RequestContext) extends Actor {

  import TimezoneService._

  implicit val system = context.system
  import system.dispatcher
  val log = Logging(system, getClass)

  def receive = {
    case Process(long,lat,timestamp) =>
      process(long,lat,timestamp)
      context.stop(self)
  }

  def process(long: Double, lat: Double, timestamp: String) = { 

    log.info("Requesting timezone long: {}, lat: {}, timestamp: {}", long, lat, timestamp)

    import TimezoneJsonProtocol._
    import SprayJsonSupport._
    val pipeline = sendReceive ~> unmarshal[GoogleTimezoneApiResult[Timezone]]

    val responseFuture = pipeline {
      Get(s"https://maps.googleapis.com/maps/api/timezone/json?location=$long,$lat&timestamp=$timestamp&sensor=false")
    }
    responseFuture onComplete {
      case Success(GoogleTimezoneApiResult(_, _, timeZoneName)) =>
        log.info("The timezone is: {} m", timeZoneName)
        requestContext.complete(timeZoneName)

      case Failure(error) =>
        requestContext.complete(error)
    }
  }
}

Conclusion

Above we created a RESTful service with Scala and Spray. You should now be able to compile, run and call the endpoints. To compile execute `sbt compile`. To run execute `sbt run`. And to call the endpoints try the following in your favorite browser (we’ve only implemented GET requests).

You should also verify our tests work by executing `sbt test`. Now, take what you’ve learned here and make something great.

If you want to get your hands on a complete implementation checkout the project at github.com/gagnechris/SprayApiDemo.