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.
It would be helpful if you could show sample requests with the server you created!
LikeLike
Hi Daniel, sorry for the SURE LATE reply!
I managed to find the time to write a README on how to run and use the application. Please have a look here: https://github.com/DanielaSfregola/quiz-management-service/tree/master/tutorial-1
Cheers,
Daniela
LikeLike
Reblogged this on Dinesh Ram Kali..
LikeLike