REST in a Scalatra Service
In our previous blog post we went through the official demo to create a basic Scalatra service http://honstain.com/scalatra-giter8/. Now we want to expand that to implement a RESTful API.
WARNING - I am not a Scalatra expert, this blog is meant to document my journey of discovery as I work towards a more fully featured service.
Adding Support for JSON
The best reference I found here is to follow the guide for http://scalatra.org/guides/2.3/formats/json.html. By adding two new dependencies to the build.sbt
file for scalatra-json and json4s-jackson we will have everything we need to get started.
I started out with an extremely basic implementation of a system to track inventory (there is no persistence layer set up and everything is kept in memory in a naive fashion).
import org.scalatra._
// JSON-related libraries
import org.json4s.{DefaultFormats, Formats}
// JSON handling support from Scalatra
import org.scalatra.json._
class ToyInventory extends ScalatraServlet with JacksonJsonSupport {
protected implicit val jsonFormats: Formats = DefaultFormats
before() {
contentType = formats("json")
}
get("/") {
InventoryData.all
}
post("/") {
val newInventory = parsedBody.extract[Inventory]
InventoryData.all = newInventory :: InventoryData.all
newInventory
}
}
case class Inventory(sku: String, qty: Int, description: String)
object InventoryData {
var all = List(
Inventory("ZL101", 1, "Black shoes"),
Inventory("ZL102", 0, "Red dress"),
Inventory("ZL103", 4, "Block of wood"),
)
}
I won't try to duplicate the information in the official guide, but we get support for JSON by mixing in the JacksonJsonSupport
trait, configuring a before()
filter, and using an implicit to specific desired format (a quick review of the README on https://github.com/json4s/json4s could be helpful here).
The result here is that now we have a toy example that we can GET and POST to. I have included the Postman examples below (Postman is my preferred tool for interacting with RESTful services).
Adding Tests to Our New Endpoints
At this stage I have created two endpoints for this toy inventory system:
- GET /
- POST /
If you used the giter8 template to generate the service, you will already have a basic test, you can use IntelliJ to run the tests for you (it gave me more than one choice, I wanted to use ScalaTest).
The test passes but it only verifies that the GET / endpoint returns an HTTP response code of 200. Lets start by adding another test for the POST. I referenced this to convert my Inventory object to JSON https://commitlogs.com/2017/01/14/serialize-deserialize-json-with-json4s-in-scala/
import org.json4s.DefaultFormats
import org.scalatra.test.scalatest._
import org.json4s.jackson.Serialization.write
class ToyInventoryTests extends ScalatraFunSuite {
implicit val formats = DefaultFormats
addServlet(classOf[ToyInventory], "/*")
test("GET / on ToyInventory should return status 200") {
get("/") {
status should equal (200)
}
}
test("POST / on ToyInventory should return status 200") {
post("/", write(Inventory("ZL104", 3, "Pine Block of wood"))) {
status should equal (200)
}
}
}
What if we want to verify the response as well? We can add additional tests and validate the body of the response. You might chose to consolidate your tests more, I kept the status code check separate from the body check in an effort to help the reader.
package org.bitbucket.honstain.app
import org.json4s.DefaultFormats
import org.scalatra.test.scalatest._
import org.json4s.jackson.Serialization.write
class ToyInventoryTests extends ScalatraFunSuite {
implicit val formats = DefaultFormats
addServlet(classOf[ToyInventory], "/*")
test("GET / on ToyInventory should return status 200") {
get("/") {
status should equal (200)
}
}
test("GET / on ToyInventory should return inventory list") {
get("/") {
val expectedResult = List(
Inventory("ZL101", 1, "Black shoes"),
Inventory("ZL102", 0, "Red dress"),
Inventory("ZL103", 4, "Block of wood"),
)
body should equal (write(expectedResult))
}
}
test("POST / on ToyInventory should return status 200") {
val newInventory = Inventory("ZL104", 3, "Pine Block of wood")
post("/", write(Inventory("ZL104", 3, "Pine Block of wood"))) {
status should equal (200)
}
}
test("POST / on ToyInventory should return newly created inventory") {
val newInventory = Inventory("ZL104", 3, "Pine Block of wood")
post("/", write(Inventory("ZL104", 3, "Pine Block of wood"))) {
// Using a string representation of the expected Inventory for demonstration purposes.
body should equal ("""{"sku":"ZL104","qty":3,"description":"Pine Block of wood"}""")
}
}
}
We have used the ScalatraFunSuite which supports a simple structure for tests, I would recommend taking a look at the ScalaTest documentation for FunSuite http://www.scalatest.org/getting_started_with_fun_suite
You can source for these changes in the project repo here: https://bitbucket.org/honstain/scalatra-example-blog/commits/ffda1e40fc8db9ae79beaea123d542459b6d3682
References I found Helpful During This Guide:
- Scalatra Documentation: http://scalatra.org/guides/2.3/formats/json.html
- JSON
- http://json4s.org/
- https://github.com/json4s/json4s
- Serialization https://commitlogs.com/2017/01/14/serialize-deserialize-json-with-json4s-in-scala/
- Manipulating lists https://alvinalexander.com/scala/how-add-elements-to-a-list-in-scala-listbuffer-immutable