How to build a Scala REST CRUD application with Spray

In previous articles we have described how to build a REST Api with Spray and how to (de)serialize case classes with json4s. However, in order to keep things simple, we didn’t always do things as suggested by spray.io.

In this article we will redeem ourselves and we will describe how to build a REST CRUD application in Spray, taking full advantage of the tools offered by the Spray’s tool kit.

All the code shown in this tutorial can be found on GitHub.

Our CRUD Application

A REST CRUD application is an application that manipulated entities using 4 key operations: create, retrieve, update, delete.
In this tutorial we will describe how to create a simple REST CRUD application to manage question entities.
A question has the following fields: id, title, text. We are going to use json4s to translate it to the following case class (for more information on how to use json4s, have a look here):

case class Question(id: String, title: String, text: String)

Also, to keep things simple, we are not going to store the entities in a database but we will simply keep them in memory. In this tutorial the class QuestionService simulates a persistent storage by storing all the questions in a Vector (see its complete code here):

package com.danielasfregola.quiz.management.services

import com.danielasfregola.quiz.management.entities.{Question, QuestionUpdate}
import scala.concurrent.{ExecutionContext, Future}

class QuestionService(implicit val executionContext: ExecutionContext) {

  var questions = Vector.empty[Question]

  def createQuestion(question: Question): Future[Option[String]] = ...

  def getQuestion(id: String): Future[Option[Question]] = ...

  def updateQuestion(id: String, update: QuestionUpdate): Future[Option[Question]] = ...
    
  def deleteQuestion(id: String): Future[Unit] = ...

}

POST – Create a Question

Usage

The first task of our application is to define an endpoint to create a question entity.
According to the REST protocol, an entity is created through a POST request that should reply with a 201 (Created) HTTP status code. Also, a Location Header with the URI that identifies the location of the new entity should be returned.
Note that a POST request is non-idempotent: if the entity already exists or cannot be created, we should return an HTTP error status code.

For our questions application, this can be translated in the following curl command:

curl -v -H "Content-Type: application/json" \
   -X POST http://localhost:5000/questions \
   -d '{"id": "test", "title": "MyTitle", "text":"The text of my question"}'

The first time we make the request, we should get a reply similar to the following:

*   Trying ::1...
* Connected to localhost (::1) port 5000 (#0)
> POST /questions HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.43.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 68
> 
* upload completely sent off: 68 out of 68 bytes
< HTTP/1.1 201 Created
< Server: Quiz Management Service REST API
< Date: Sat, 21 Nov 2015 11:37:11 GMT
< Location: http://localhost:5000/questions/test
< Content-Length: 0
<
* Connection #0 to host localhost left intact

If we repeat the request again, we will get an HTTP response with a 409 (Conflict) status code as the entity already exists:

*   Trying ::1...
* Connected to localhost (::1) port 5000 (#0)
> POST /questions HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.43.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 68
> 
* upload completely sent off: 68 out of 68 bytes
< HTTP/1.1 409 Conflict
< Server: Quiz Management Service REST API
< Date: Sat, 21 Nov 2015 11:53:34 GMT
< Content-Length: 0
< 

Implementation

Spray has several methods to complete a generic result and convert it into a Route (see the Spray Documentation for more information). However, there isn’t a standard function to transform a result into a Location Header….so we are going to write one! 😀
Note that our implementation is tailored to the behaviour of our system: when QuestionService creates a question, it returns a Future[Option[T]] and, if the returned option is not defined, we want to return a different HTTP status code.

package com.danielasfregola.quiz.management.routing

import com.danielasfregola.quiz.management.serializers.JsonSupport
import spray.http.HttpHeaders
import spray.routing._
import scala.concurrent.{ExecutionContext, Future}

trait MyHttpService extends HttpService with JsonSupport {

  implicit val executionContext: ExecutionContext

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

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

We can now put everything together and define the endpoint to create a question entity:

package com.danielasfregola.quiz.management.resources

import com.danielasfregola.quiz.management.entities.{QuestionUpdate, Question}
import com.danielasfregola.quiz.management.routing.MyHttpService
import com.danielasfregola.quiz.management.services.QuestionService
import spray.routing._

trait QuestionResource extends MyHttpService {

  val questionService: QuestionService

  def questionRoutes: Route = pathPrefix("questions") {
    pathEnd {
      post {
        entity(as[Question]) { question =>
          completeWithLocationHeader(
            resourceId = questionService.createQuestion(question),
            ifDefinedStatus = 201, ifEmptyStatus = 409)
          }
        }
    } ~
    ...
  }

}

GET – Retrieve a Question

Usage

Now that we have created a question, we can retrieve it by performing a GET request to the URI that identifies the entity (i.e.: the one returned in the Location Header). The request should respond with either a 200 (OK) HTTP status code with a body containing the question entity or a 404 (NotFound) HTTP status code with empty body.

For example, we can get an existing question with the following curl command…

curl -v http://localhost:5000/questions/test

…and it should return something similar to the following:

*   Trying ::1...
* Connected to localhost (::1) port 5000 (#0)
> GET /questions/test HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Server: Quiz Management Service REST API
< Date: Sat, 21 Nov 2015 12:23:34 GMT
< Content-Type: application/json; charset=UTF-8
< Content-Length: 64
< 
* Connection #0 to host localhost left intact
{"id":"test","title":"MyTitle","text":"The text of my question"}

Moreover, if we request an entity that doesn’t exists…

curl -v http://localhost:5000/questions/non-existing-question

….we should get a 404 error code:

*   Trying ::1...
* Connected to localhost (::1) port 5000 (#0)
> GET /questions/non-existing-question HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 404 Not Found
< Server: Quiz Management Service REST API
< Date: Sat, 21 Nov 2015 12:25:43 GMT
< Content-Length: 0
< 
* Connection #0 to host localhost left intact

Implementation

This behaviour can be easily be achieved with Spray as it will automatically complete optional values:
– if the option is defined, complete will transform it in a HTTP response with status 200 (OK) status code and a body containing the entity json representation.
– if the option is empty, it will just return a HTTP response with status 404 (NotFound) status code.

package com.danielasfregola.quiz.management.resources

import com.danielasfregola.quiz.management.entities.{QuestionUpdate, Question}
import com.danielasfregola.quiz.management.routing.MyHttpService
import com.danielasfregola.quiz.management.services.QuestionService
import spray.routing._

trait QuestionResource extends MyHttpService {

  val questionService: QuestionService

  def questionRoutes: Route = pathPrefix("questions") {
    ... ~
    path(Segment) { id =>
      get {
        complete(questionService.getQuestion(id))
      } ~
      ...
    }
  }
}

PUT – Update a Question

Usage

When updating an entity, we should use a PUT request. Also, we should send only the fields that we want to update, not the whole object. Not only this will make the usage of our API easier, but it will also reduce potential concurrency issues.
If the update goes through, we should get a HTTP response with a 200 (OK) status code with the updated entity in the body. On the other side, if the update was not possible, for example because the entity no longer exists, we should get a HTTP response with status 404 (NotFound) and an empty body.
Note that a PUT request is idempotent: performing the update multiple times should already return the same result.

In our application we can update the question entity with the following curl command…

curl -v -H "Content-Type: application/json" \
   -X PUT http://localhost:5000/questions/test \
   -d '{"text":"Another text"}'

….and get the following reply:

*   Trying ::1...
* Connected to localhost (::1) port 5000 (#0)
> PUT /questions/test HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.43.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 23
> 
* upload completely sent off: 23 out of 23 bytes
< HTTP/1.1 200 OK
< Server: Quiz Management Service REST API
< Date: Sat, 21 Nov 2015 12:44:03 GMT
< Content-Type: application/json; charset=UTF-8
< Content-Length: 53
< 
* Connection #0 to host localhost left intact
{"id":"test","title":"MyTitle","text":"Another text"}

Similarly, if we try to update a resource that doesn’t exist…

curl -v -H "Content-Type: application/json" \
   -X PUT http://localhost:5000/questions/non-existing-question \
   -d '{"text":"Another text"}'

…we should get a 404 (NotFound) error code:

*   Trying ::1...
* Connected to localhost (::1) port 5000 (#0)
> PUT /questions/non-existing-question HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.43.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 23
> 
* upload completely sent off: 23 out of 23 bytes
< HTTP/1.1 404 Not Found
< Server: Quiz Management Service REST API
< Date: Sat, 21 Nov 2015 12:46:15 GMT
< Content-Length: 0
< 
* Connection #0 to host localhost left intact

Implementation

As explained in the previous section, we want the client of our API to send just the fields to update, not the whole entity. In order to achieve this, we will deserialize the body of our PUT request to the following case class:

case class QuestionUpdate(title: Option[String], text: Option[String])

Note that we decided not to allow our clients to update the field id, as it is used to locate the entity.

Similarly to what we did for the GET request, Spray does all the work for us:

package com.danielasfregola.quiz.management.resources

import com.danielasfregola.quiz.management.entities.{QuestionUpdate, Question}
import com.danielasfregola.quiz.management.routing.MyHttpService
import com.danielasfregola.quiz.management.services.QuestionService
import spray.routing._

trait QuestionResource extends MyHttpService {

  val questionService: QuestionService

  def questionRoutes: Route = pathPrefix("questions") {
    ... ~
    path(Segment) { 
      ... ~
      put {
        entity(as[QuestionUpdate]) { update =>
          complete(questionService.updateQuestion(id, update))
        }
      } ~
      ...
    }
  }
}

DELETE – Delete a Question

Usage

Finally, we want to have an endpoint to delete a question entity. This can be achieved by sending a DELETE request to the URI that identifies the entity that should reply with a 204 (NoContent) status code once the operation has been completed. Note that DELETE is idempotent, so deleting a resource that has been already deleted should still return an HTTP response with a 204 (NoContent) status code and an empty body.

For example, we can delete the question test with the following…

curl -v -X DELETE http://localhost:5000/questions/test

…and get the following result back:

*   Trying ::1...
* Connected to localhost (::1) port 5000 (#0)
> DELETE /questions/test HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 204 No Content
< Server: Quiz Management Service REST API
< Date: Sat, 21 Nov 2015 12:58:30 GMT
< Content-Type: application/json; charset=UTF-8
< Content-Length: 2
< 
* Excess found in a non pipelined read: excess = 2 url = /questions/non-existing-question (zero-length body)
* Connection #0 to host localhost left intact

Implementation

Once again, Spray makes our life really easy as all we have to do in order to define an endpoint to delete a question is just to reuse already defined functions in the Spray’s tool kit:

package com.danielasfregola.quiz.management.resources

import com.danielasfregola.quiz.management.entities.{QuestionUpdate, Question}
import com.danielasfregola.quiz.management.routing.MyHttpService
import com.danielasfregola.quiz.management.services.QuestionService
import spray.routing._

trait QuestionResource extends MyHttpService {

  val questionService: QuestionService

  def questionRoutes: Route = pathPrefix("questions") {
    ... ~
    path(Segment) { id =>
      ... ~
      delete {
        complete(204, questionService.deleteQuestion(id))
      }
    }
  }
}

Summary

In this article we have described what a REST CRUD application is. Also, we have provided a simple tutorial on how to create a simple CRUD application using Spray. The code of the application analysed can be found on GitHub.

Spray: how to (de)serialize objects with json4s

A while ago I wrote an article on how to build a REST api with Spray, where I used the Spray facilities to serialize and deserialize objects to and from JSON.

This article will analyze how we can make the (de)serialization of our case classes easier, by using a library called json4s.

All the code produced in this tutorial can be found here.

Why Json4s?

Although the Spray way of (de)serializing objects works out-of-the-box, it is not always super convenient to use.

In fact, for every entity we need to specify the corresponding JSON format. See for example our QuizProtocol object from the previous tutorial:

// file QuizProtocol.scala
 ...  
  object Quiz extends DefaultJsonProtocol {
    implicit val format = jsonFormat3(Quiz.apply)
  }

  object Question extends DefaultJsonProtocol {
    implicit val format = jsonFormat2(Question.apply)
  }

  object Answer extends DefaultJsonProtocol {
    implicit val format = jsonFormat1(Answer.apply)
  }
...
/* We need to do it for each object
 * and we need to specify the number of fields to serialize
 */

Json4s is an easy-to-use library that automatically (de)serializes case classes into JSON objects: by using this library we will avoid the need of specifying a (de)serialization format for every case class.

Set up

First of all, we need to import the library by adding the dependencies to our build.sbt file:

// build.sbt
libraryDependencies ++= {
  ...
  val Json4sVersion = "3.2.11"

  Seq(
    ...
    "org.json4s" %% "json4s-native" % Json4sVersion,
    "org.json4s" %% "json4s-ext" % Json4sVersion,
    ...
  )
}

We now need to tell json4s about any customization we would like in our serializers.

For example, we can define a different date format to use:

// file JsonSupport.scala
package com.danielasfregola.quiz.management.serializers

import java.text.SimpleDateFormat

import org.json4s.ext.JodaTimeSerializers
import org.json4s.{DefaultFormats, Formats}
import spray.httpx.Json4sSupport

trait JsonSupport extends Json4sSupport {

  implicit def json4sFormats: Formats = customDateFormat ++ JodaTimeSerializers.all

  val customDateFormat = new DefaultFormats {
    override def dateFormatter = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss")
  }
  
}

Also, note that the package json4s-ext offers serializers for types that, although not standard, are quite common. For example, if you use joda.time you will need to add a specific group of serializers, called JodaTimeSerializers.

Json4s Custom Serializers

What happens if you need to serialize a type that is not supported by json4s? Do not dispare! In fact, json4s allows you to specify custom serializers quite easily.

For example, let’s assume that your API uses Timestamp dates and that when, (de)serializing a date in JSON, it should be converted to milliseconds from the unix epoc (i.e.: 1970-01-01 00:00:00 UTC). All you have to do is to create a custom serializer as following:

// file CustomerSerializers.scala
package com.danielasfregola.quiz.management.serializers

import java.sql.Timestamp

import org.json4s.CustomSerializer
import org.json4s.JsonAST.{JInt, JNull}

object CustomSerializers {
  val all = List(CustomTimestampSerializer)
}

case object CustomTimestampSerializer extends CustomSerializer[Timestamp](format =>
  ({
    case JInt(x) => new Timestamp(x.longValue * 1000)
    case JNull => null
  },
    {
      case date: Timestamp => JInt(date.getTime / 1000)
    }))

Don’t forget to add your custom serializers as part of your (de)serialization configuration:

// file JsonSupport.scala
...

trait JsonSupport extends Json4sSupport {

  implicit def json4sFormats: Formats = ... ++ CustomSerializers.all

  ...
}

Usage

Now that the set up is completed we just need to extend the JsonSupport trait every time a JSON format for (de)serialization is required.

For example, our QuitProtocol object now looks a lot simpler than before:

// file QuizProtocol.scala
package com.danielasfregola.quiz.management

import com.danielasfregola.quiz.management.serializers.JsonSupport

object QuizProtocol extends JsonSupport {

  case class Quiz(id: String, question: String, correctAnswer: String)
  
  case object QuizCreated
  
  case object QuizAlreadyExists
  
  case object QuizDeleted
  
  case class Question(id: String, question: String)
  
  case object QuestionNotFound
  
  case class Answer(answer: String)
  
  case object CorrectAnswer
  
  case object WrongAnswer

  implicit def toQuestion(quiz: Quiz): Question = Question(id = quiz.id, question = quiz.question)

  implicit def toAnswer(quiz: Quiz): Answer = Answer(answer = quiz.correctAnswer)
}

Summary

Previously, we have described how to create a REST API using Spray. This article has simplified how to perform (de)serialization in Spray, by using the json4s library.

All the code produced in this tutorial can be found here.

How to Create a Spray Custom Authenticator

Spray has a few standard authenticators already built in. However, what if the authentication we need is not supported? In this article we will describe how to create a custom authenticator in Spray. In particular, we will implement a custom client id/secret token-based authentication.

Spray Authentication: how it works

Spray has already some built in authentication systems. For example, it supports HTTP Basic Authentication: have a look at their documentation here. What if the authentication logic that we need is not supported? Luckily, Spray allows the implementation of custom authenticators.

The authenticate directive has two signatures:

  def authenticate[T](auth: ⇒ Future[Authentication[T]])(implicit executor: ExecutionContext): Directive1[T]
  def authenticate[T](auth: ContextAuthenticator[T])(implicit executor: ExecutionContext): Directive1[T]

where ContextAuthenticator and Authentication are aliases for the following types:

// from spray.routing.authentication package object
...
  type Authentication[T] = Either[Rejection, T]
  type ContextAuthenticator[T] = RequestContext ⇒ Future[Authentication[T]]
...

In other words, a ContextAuthenticator is a function that takes a RequestContext (i.e.: a wrapper that contains all the information about the received request) and it uses it to either authenticate or reject the request.

Cient Id/Secret Custom Authenticator

Our goal is to create a custom authenticator that looks at the query parameters of a request, it extracts some client_id and client_secret and it authorizes the request if they are correct.

For the purposes of this tutorial we have decided to focusing on the second signature of the authenticate directive, so we will define a ContextAuthenticator to use with the directive.

First, let’s create a data container for our credentials and an auxiliary method to extract the id/secret from the request context:

  case class Credentials(id: String, secret: String)

  private def extractCredentials(ctx: RequestContext): Option[Credentials] = {
    val queryParams = ctx.request.uri.query.toMap
    for {
      id <- queryParams.get("client_id")
      secret <- queryParams.get("client_secret")
    } yield Credentials(id, secret)
  }

We can now define our ContextAuthenticator as following, keeping in mind that Authentication[T] is just a type alias for Either[Rejection, T]:

 val authenticator: ContextAuthenticator[Unit] = { ctx =>
      Future {
        val maybeCredentials = extractCredentials(ctx)
          maybeCredentials.fold[authentication.Authentication[Unit]](
            Left(AuthenticationFailedRejection(CredentialsMissing, List()))
          )( credentials =>
              credentials match {
                case AppCredentials("my-client-id", "my-client-super-secret") => Right()
                case _ => Left(AuthenticationFailedRejection(CredentialsRejected, List()))
              }
          )
      }
  }

If any of the authentication query parameters are missing, the following response is returned:

Status: 401 Unauthorized
Body: The resource requires authentication, which was not supplied with the request

If the credentials are provided but they are not correct, the client will see the following message:

Status: 401 Unauthorized
Body: The supplied authentication is invalid

Finally, if the credentials are correct, the request will be authorized and satisfied if possible.

Note that in a more realistic case scenario, you would probably return a User entity instead of Unit. Moreover, rather than having the correct credentials hard-coded in the code, they should be either configurable or stored in a proper data storage.

We are now ready to use our custom authenticator!

For example, to make all our endpoints use our custom authenticator we could do something similar to the following:

 def routes: Route = 
    sealRoute {
        authenticate(authenticator) { authenticated =>
            firstResourceRoutes ~
              secondResourceRoutes
          }
      }
    }

Summary

Spray allows the implementation of custom authenticators. This article provides an example of how this feature can be used to implement a client id/secret token based authentication.

How to build a REST api with Spray and Akka

Spray is a library written on top of Akka and Scala that allows to quickly create REST interfaces. It is becoming more and more popular in the Scala community because it is easy to use and performant thanks to this asynchronous, actor-based model. This article describes how to efficiently exploit Spray to create a simple REST api.

All the code produced in this tutorial, together with a quick guide on how to run and use the application, can be found here.

Update: I wrote some follow ups on this article:
1) In this article, I used the default Spray facilities for (de)serializing objects from and to JSON. In this follow-up article, I’m showing how to make (de)serialization in Spray simpler by using the json4s library.
2) Also, this article doesn’t always respect the REST protocol or the spray way of completing an endpoint. In the following article, I explain how to build a REST CRUD application in Spray, making full advantage of the Spray’s tool kit.

Our Goal

For this tutorial we want to build a REST interface to manage quizzes. In particular we would like our interface to do the following:
– Create a quiz
– Delete a quiz
– Get a random question
– Get a question by id
– Answer a question

In the following sections we will analyse how to achieve these requirements. In particular, we will focus on how to create and delete a quiz entity. The described pattern can be re-used to achieve all the other requirements: describing all the requirements one by one would not bring any extra value to the tutorial itself. Please, have a look at the GitHub repository for the complete code of this tutorial and examples on how to use the application.

Project Metadata

First we need to set up our project. Let’s add some metadata: its name and version, its organization, what scala and SBT versions we intend to use.

// file build.sbt
name := "quiz-management-service"

version := "0.1"

organization := "com.danielasfregola"

scalaVersion := "2.11.5"
// file project/build.properties
sbt.version=0.13.6

Plugins

Next step: make our life a lot easier! By importing the following plugins, we just need to run the sbt assembly command to assembly our code: it will compile the code, run all the tests and produce an executable java jar.

// file project/plugins.sbt
resolvers += Classpaths.typesafeResolver

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.12.0")

addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.0.0-M4")

addSbtPlugin("com.typesafe.sbt" % "sbt-multi-jvm" % "0.3.9")
// file build.sbt
enablePlugins(JavaServerAppPackaging)
...

// Assembly settings
mainClass in Global := Some("com.danielasfregola.quiz.management.Main")

jarName in assembly := "quiz-management-server.jar"

To compile, test and assembly the code:

cd quiz-management-service/spray-akka
sbt assembly

The produced jar will be called “quiz-management-server.jar” and it will be use the main class located in the package “com.danielasfregola.quiz.management”. To run our service (by default on http://localhost:5000), we just need to type the following command:

java -jar target/scala-2.11/quiz-management-service.jar

Spray Dependencies

Almost there with the boring part! We now have to include the spray libraries that we are going to use:

// file build.sbt
resolvers ++= Seq("Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/",
"Spray Repository" at "http://repo.spray.io")

libraryDependencies ++= {
    val akkaVersion = "2.3.9"
    val sprayVersion = "1.3.2"
    Seq(
        "com.typesafe.akka" %% "akka-actor" % akkaVersion,
        "io.spray" %% "spray-can" % sprayVersion,
        "io.spray" %% "spray-routing" % sprayVersion,
        "io.spray" %% "spray-json" % "1.3.1",
        "com.typesafe.akka" %% "akka-slf4j" % akkaVersion,
        "ch.qos.logback" % "logback-classic" % "1.1.2",
        "com.typesafe.akka" %% "akka-testkit" % akkaVersion % "test",
        "io.spray" %% "spray-testkit" % sprayVersion % "test",
        "org.specs2" %% "specs2" % "2.3.13" % "test"
    )
}

Main Class

The first thing to do in our Main class is to load our system configurations. Then, we create our actor system called “quiz-management-service” that contains our http interface — remember that Spray is actor-based! Finally, our actor system is bound to an HTTP port.

package com.danielasfregola.quiz.management

import akka.actor._
import akka.io.IO
import akka.pattern.ask
import akka.util.Timeout
import com.typesafe.config.ConfigFactory
import spray.can.Http

import scala.concurrent.duration._

object Main extends App {
    val config = ConfigFactory.load()
    val host = config.getString("http.host")
    val port = config.getInt("http.port")
    
    implicit val system = ActorSystem("quiz-management-service")
    
    val api = system.actorOf(Props(new RestInterface()), "httpInterface")
    
    implicit val executionContext = system.dispatcher
    implicit val timeout = Timeout(10 seconds)
    
    IO(Http).ask(Http.Bind(listener = api, interface = host, port = port))
    .mapTo[Http.Event]
    .map {
        case Http.Bound(address) =>
            println(s"REST interface bound to $address")
        case Http.CommandFailed(cmd) =>
            println("REST interface could not bind to " +
            s"$host:$port, ${cmd.failureMessage}")
        system.shutdown()
    }
}

Rest Interface Class

The Rest interface class is an HttpServiceActor: every time it receives an HTTP request, it tries to match it to a routing defined in our RestApi trait. When a matching is found, the action defined is executed and the result sent to a Responder actor. The Responder actor is responsible for sending back a meaningful HTTP response with an appropriate status code and body. Every time we receive a valid HTTP request, we create an instance of the Responder actor and then kill it once the request has been completed.

package com.danielasfregola.quiz.management

import akka.actor._
import akka.util.Timeout
import spray.http.StatusCodes
import spray.httpx.SprayJsonSupport._
import spray.routing._

import scala.concurrent.duration._
import scala.language.postfixOps

class RestInterface extends HttpServiceActor
with RestApi {

    def receive = runRoute(routes)
}

trait RestApi extends HttpService with ActorLogging { actor: Actor =>

    implicit val timeout = Timeout(10 seconds)
    
    var quizzes = Vector[Quiz]()
    
    def routes: Route = ???
}

class Responder(requestContext:RequestContext) extends Actor with ActorLogging {

    def receive = ???
    
    private def killYourself = self ! PoisonPill

}

For the purposes of this tutorial, we will store all the data in a vector: this is not an ideal solution for a production system, but it allows this article to focus on how Spray works. In a more realistic context, we should delegate actions to other classes — or even better actors! — and store the data in a persistent data storage (e.g.: database).

How to create and delete a quiz

Our interface needs to provide the functionalities of creating and deleting a quiz entity. This can be translated in the following REST calls:
– POST to URI /quizzes with JSON body: {"id": "my_quiz_id", "question": "my_question", "correctAnswer": "my_answer"}
– DELETE to URI /quizzes/my_quiz_id with no body

First, we need to define what a Quiz is and the messages that we can send to the Responder actor. This can easily be achieved by using case classes and objects:

// file src/main/scala/com/danielasfregola/quiz/management/QuizProtocol.scala
...
// entity: quiz
case class Quiz(id: String, question: String, correctAnswer: String)

// message: quiz has been created
case object QuizCreated

// message: quiz cannot be created because it already exists
case object QuizAlreadyExists

// message: quiz has been deleted
case object QuizDeleted
...

Also, we need instructions on how to (un)marshall a Quiz case class.

// file src/main/scala/com/danielasfregola/quiz/management/QuizProtocol.scala
...
object Quiz extends DefaultJsonProtocol {
    implicit val format = jsonFormat3(Quiz.apply)
}
...

The RestApi trait defines the routing and the operation to execute. For each matched HTTP request we create an instance of the Responder actor: once the result of the executed operation is available we send it to the Responder actor.

....

var quizzes = Vector[Quiz]()

def routes: Route =

pathPrefix("quizzes") {
    pathEnd {
        post {
            entity(as[Quiz]) { quiz => requestContext =>
                val responder = createResponder(requestContext)
                createQuiz(quiz) match {
                    case true => responder ! QuizCreated
                    case _ => responder ! QuizAlreadyExists
                }
            }
        }
    } ~
    path(Segment) { id =>
        delete { requestContext =>
            val responder = createResponder(requestContext)
            deleteQuiz(id)
            responder ! QuizDeleted
        }
    }
}
...

private def createQuiz(quiz: Quiz): Boolean = {
    val doesNotExist = !quizzes.exists(_.id == quiz.id)
    if (doesNotExist) quizzes = quizzes :+ quiz
    doesNotExist
}

private def deleteQuiz(id: String): Unit = {
    quizzes = quizzes.filterNot(_.id == id)
}

private def createResponder(requestContext:RequestContext) = {
    context.actorOf(Props(new Responder(requestContext)))
}

When the Responder actor receives a message it maps it to a meaningful HTTP response: it sends it back to the HTTP requester and it kills itself ( 😥 ).

// file src/main/scala/com/danielasfregola/quiz/management/RestInterface.scala
class Responder(requestContext:RequestContext) extends Actor with ActorLogging {
import com.danielasfregola.quiz.management.QuizProtocol._

def receive = {

case QuizCreated =>
    requestContext.complete(StatusCodes.Created)
    killYourself

case QuizDeleted =>
    requestContext.complete(StatusCodes.OK)
    killYourself

case QuizAlreadyExists =>
    requestContext.complete(StatusCodes.Conflict)
    killYourself

....
}

private def killYourself = self ! PoisonPill

Summary

This article has described how to use Spray to create a REST interface. Spray is a library that uses a actor-based model to easily and efficiently implement a REST layer. Although the provided example in not applicable in a realistic system (because all the data is stored in a Vector variable!), it shows a simple pattern on how to structure a REST api using Spray: how to organise the communication between actors, the routing and the response handling.