In this second and last part, we’ll see how to use Akka HTTP to create RESTful Web Services. The idea is to expose the operations defined in the first part of this article, using HTTP and JSON.
Akka HTTP implements a full server/client-side HTTP based on top of Akka Actors. It describes itself as “a more general toolkit for providing and consuming HTTP-based services instead of a web-framework“. One interesting aspect is that it provides different abstraction levels for doing the same thing in most of the scenarios, i.e., provides high and low level APIs. Akka HTTP is pretty much the continuation of the Spray Project and Spray’s creators are part of Akka HTTP team. In our example, we’ll be using the high level Routing DSL to expose services, which allows routes to be defined and composed by directives. We’ll also use the Spray Json project to provide the infrastructure needed to use JSON with Akka HTTP.
Before jumping into the definition of our Akka HTTP Route, let’s see what needs to be done to allow us to use the Customer entity in JSON requests/responses. Let’s change the Customer.scala file to be like this:
Customer.scala
package com.lucianomolinari.akkahttpredis import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport import spray.json.DefaultJsonProtocol /** * Represents the domain model of a Customer. * <p> * A Customer only has an id once he's persisted, so his id field is of type Option. */ case class Customer(id: Option[Long], name: String, age: Int) { def this(name: String, age: Int) = this(None, name, age) } case class CustomerId(id: Long) /** * Wraps all the required json formatting stuff, to allow a Customer/CustomerId to be automatically * marshalled/unmarshalled from an HTTP Request and to an HTTP response. */ trait CustomerJsonProtocol extends DefaultJsonProtocol with SprayJsonSupport { implicit val customerFormat = jsonFormat3(Customer) implicit val customerIdFormat = jsonFormat1(CustomerId) }
As you can see we added the case class CustomerId and a trait called CustomerJsonProtocol. The CustomerId class will be used as the reply for the POST operation, where we want to return the ID of the customer created.
Spray Json library provides an easy way to (un)marshall case classes via the usage of jsonFormatX methods, where X is the number of attributes your case class has. In our case, we declare customerFormat using jsonFormat3 as Customer has 3 attributes and customerIdFormat using jsonFormat1 as CustomerId only has one single attribute. These formatters need to be declared as implicit, as this is implicitly needed by Akka HTTP. This pattern will be enough in most of the scenarios, but of course you can customize the way you (un)marshall your classes. More details can be seen at Spray Json project page.
Now that all the plumbing for using JSON is ready, we can move forward to the implementation of our REST Services:
CustomerRest.scala
package com.lucianomolinari.akkahttpredis import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route /** * Responsible for configuring the HTTP route to expose operations around [[Customer]]. * <p> * The operations available are: * <ul> * <li>GET /api/customer/ID => Finds and returns the Customer given by ID * <li>POST /api/customer => Persists a new Customer. * <li>DELETE /api/customer/ID => Removes the Customer given by ID * </ul> * * @param customerRepository The repository of customers. */ class CustomerRest(customerRepository: CustomerRepository) extends CustomerJsonProtocol { val route: Route = logRequestResult("customer-service") { pathPrefix("api") { get { path("customer" / LongNumber) { id => onSuccess(customerRepository.find(id)) { case Some(customer) => complete(customer) case None => complete(StatusCodes.NotFound) } } } ~ post { (path("customer") & entity(as[Customer])) { customer => onSuccess(customerRepository.add(customer)) { case customerAdded => complete(CustomerId(customerAdded.id.get)) } } } ~ delete { path("customer" / LongNumber) { id => onSuccess(customerRepository.remove(id)) { case true => complete(StatusCodes.OK) case false => complete(StatusCodes.NotFound) } } } } } }
This class extends the trait CustomerJsonProtocol to make the implicits formatters available for the route. You can also expose them in an object and then import it instead of extending the trait, the result will be the same. As this service will be responsible for exposing customer management feature via HTTP, it needs a way to get access to a CustomerRepository instance. Here we decided to receive it as an argument in the constructor.
In the definition of the route, you can see how composable Akka HTTP directives are. We start off by using the logRequestResult one. This directive is responsible for automatically logging all the requests and responses and the value “customer-service” will be used as marker in the log messages.
After that, we wrap all HTTP operations using the pathPrefix directive, meaning that all of our requests will be mapped under “/api/”.
Now, we can start exposing each one of the operations.
- The first one is a GET operation, that will listen to “/customer/<ID>“. In this case we’re using the path directive along with the path matchers DSL. The ID received as part of the HTTP request is mapped into “id” and then the CustomerRepository.find is invoked. If you remember well, this method returns a Future[Option[Customer]], so we need to handle this. An useful directive in this case is the onSuccess. It basically waits until the Future is completed, gets the result from it and pass to the Route. Then we just need to check whether a Customer was found or not. If so, we return the Customer using the complete directive. See how we don’t need to worry how Customer will be converted to Json, as this has already been provided by the implicit converters defined in CustomerJsonProtocol. If no Customer is found, a simple NotFound (404) is returned.
- The POST operation is responsible for adding a new Customer, so we need to receive him as part of the request. This is done by using the entity directive. Notice how our JSON formatter also works for converting a JSON request to the domain object. Once we have the Customer instantiated, we just need to pass it to CustomerRepository and handle the Future with the ID.
- Then we expose a delete operation in case a Customer needs to be removed.
It’s nice to see how Akka HTTP provides a lot of different and small directives that can be put together in order to compose a bigger solution. Here we used Akka HTTP to send complete responses with the complete directive, but it also provides support to streaming use cases, where data continues flowing through the response. It’s a very powerful tool.
A few notes about the usage of logRequestResult. As pretty much everything in Akka, logging is performed asynchronously. By default Akka logs the messages to the STDOUT, which is not desirable in production environments, so we can see here how to use SLF4j to log messages to files instead. In order to change the default log level so we can see the messages when a request comes, create a file named application.conf with the following content under src/main/resources.
application.conf
akka { loglevel = DEBUG }
But how do we test this? Do we need to start a server? The answer is no, that’s not needed. Akka Route TestKit provides a trait called ScalatestRouteTest that makes our lifes much easier. You basically just need to have access to the Route you want to test. The test code can be seen below:
CustomerRestSpec.scala
package com.lucianomolinari.akkahttpredis import akka.http.scaladsl.model._ import akka.http.scaladsl.testkit.ScalatestRouteTest import org.scalatest.{BeforeAndAfter, Matchers, WordSpec} import redis.RedisClient import scala.concurrent.Await import scala.concurrent.duration._ class CustomerRestSpec extends WordSpec with Matchers with ScalatestRouteTest with BeforeAndAfter with CustomerJsonProtocol { val customerRest = new CustomerRest(new CustomerRepository()) before { val redis = RedisClient() Await.ready(redis.flushdb(), 1 second) } "The Customer Rest service" should { "return the customer when he exists" in { addCustomer(new Customer("John", 30), 1) Get("/api/customer/1") ~> customerRest.route ~> check { responseAs[Customer] shouldBe Customer(Some(1), "John", 30) } } "return NotFound when the customer doesn't exist" in { Get("/api/customer/1") ~> customerRest.route ~> check { status shouldBe StatusCodes.NotFound } } "remove a customer when he exists" in { addCustomer(new Customer("John", 30), 1) Delete("/api/customer/1") ~> customerRest.route ~> check { status shouldBe StatusCodes.OK } Get("/api/customer/1") ~> customerRest.route ~> check { status shouldBe StatusCodes.NotFound } } "return NotFound when the customer to be removed doesn't exist" in { Delete("/api/customer/1") ~> customerRest.route ~> check { status shouldBe StatusCodes.NotFound } } def addCustomer(customer: Customer, expectedId: Long) = { Post("/api/customer", customer) ~> customerRest.route ~> check { status shouldBe StatusCodes.OK responseAs[CustomerId] shouldBe CustomerId(expectedId) } } } }
This class extends/mixes a few traits, like we did previously in the first part of this article. Then we create a CustomerRest instance specifying a CustomerRepository and in the before block the DB is flushed, exactly how was done in the CustomerRepositorySpec test class. See that we could have specified a mocked version of CustomerRepository, but I prefered to go with the real code/DB.
In order to test a specific route, the following pattern/DSL is used:
Request ~> Route ~> Assertions
From within the Check/Assertion block, we have access to the HTTP Response, so we can get useful information like statusCode and the response. As we’ve mixed the CustomerJsonProtocol trait in this test class, we can (un)marshall Customer/CustomerId objects.
So far so good, but how do we start the HTTP Server so that other applications can access our Services? That’s the purpose of the class below:
CustomerAppServer.scala
package com.lucianomolinari.akkahttpredis import akka.actor.ActorSystem import akka.http.scaladsl.Http import akka.stream.ActorMaterializer import scala.io.StdIn /** * Main class. * <p> * Responsible for setting up application/dependencies and then starting Akka HTTP Server to listen on port 8080. */ object CustomerAppServer extends App { // Defines all implicit dependencies required by Akka HTTP. The ActorSystem instance is needed both by // Akka HTTP and rediscala client. implicit val system = ActorSystem("customer-system") implicit val materializer = ActorMaterializer() implicit val executionContext = system.dispatcher // Instantiates classes and their dependencies val customerRepository = new CustomerRepository() val customerRest = new CustomerRest(customerRepository) // Starts Akka HTTP server to listen on port 8080 and registers the route defined by CustomerRest val bindingFuture = Http().bindAndHandle(customerRest.route, "localhost", 8080) println(s"The server is ready to handle HTTP requests") // Listen for any enter command to stop the server/program. StdIn.readLine bindingFuture .flatMap(_.unbind()) .onComplete(_ => system.terminate()) }
We need some plumbing like defining implicit values needed by Akka HTTP and to set up both of our CustomerRepository and CustomerRest instances. Once that’s done, we can use the bindAndHandle method to start the HTTP Server on port 8080 and to expose our route. After the server is running, it can be stopped by pressing the Enter key.
You can run this class either from the IDE or by simply running “sbt run“. You should be able to test these services using CURL, Postman or any other tool that you’re familiar with. The services are available at:
- POST http://localhost:8080/api/customer passing a JSON in the body, like:
{ "name": "John", "age": 30 }
- GET http://localhost:8080/api/customer/<ID>
- DELETE http://localhost:8080/api/customer/<ID>
Error Handling
Akka HTTP has a nice default error handler mechanism in place. For example, if you try to send an invalid JSON in a POST request, like this:
{ "name": "John", "age": "a30" }
You’ll get an error like this:
The request content was malformed: Expected Int as JsNumber, but got "a30"
Notice that instead of getting some huge stack trace, Akka HTTP provides a reasonably nice message and this behaviour works out of the box. No configuration needed. Of couse it provides means in case you want to add a customized exception handling.
Summary
The main purpose of this 2 parts article was to show how to create a simple application using Akka HTTP backed by a database, in this case Redis. We could see some nice Scala Futures, such as how to handle/transform Future objects, which is a common scenario when dealing with non-blocking/async libraries, like rediscala.
Then we went through Akka HTTP to show how powerful this toolkit is and how expressive it can become by the composability of directives. Its documentation is pretty good, so it’s worth going through it. And, as usual, we saw how all of our code can be easily tested, by relying in powerfull tools like ScalaTest and Akka Route TestKit.
The complete source code for this project can be found here.
One thought on “Building a service using Akka HTTP and Redis – Part 2 of 2”