Building a Simple REST API with Scala & Play! (Part 2)

In this 3 part series, we’ll cover creating a basic Play! REST API on top of Reactive Mongo.  Full source code for this tutorial is available at https://github.com/jrodenbostel/getting-started-play-scala.

Welcome back!

If you’re coming in fresh, and need some instructions on getting the appropriate tools installed and creating a shell of an environment, please refer to part 1.  In part 2, we’ll cover adding our first API functions as asynchronous actions in a Play! controller, as well as define our first data access functions.
Because we started with a seed project, we got some bonus cruft in our application in the form of a views package.  While Scala templates are a fine way to create views for your application, for this tutorial, we’ll be building a RESTful API that responds with JSON strings. Start by deleting the ‘views’ folder at ‘app/views’ such that:
Screen Shot 2016-01-25 at 10.03.14 PM
Note that this will break the default controller that came as part of the project seed.  To remedy this, update the default controller action response to simply render text, by replacing:
Ok(views.html.index("Your new application is ready."))
with:
Ok("Your new application is ready.")

Creating the controller

Next, we’ll create our controller.  Create a new file in the ‘app/controllers’ folder named ‘Widgets.scala’.  This will house the RESTful actions associated with Widgets.  We’ll also add default methods to this controller for the RESTful actions that we’ll implement later.
package controllers

import play.api.mvc._

class Widgets extends Controller {

  def index = TODO

  def create = TODO

  def read(id: String) = TODO

  def update(id: String) = TODO

  def delete(id: String) = TODO
}
Play! comes with a default “TODO” page for controller actions that have not yet been implemented.  It’s a nice way to keep your app functioning and reloading while you’re building out functionality incrementally.  Before we can see that default “TODO” page, we must first add routes to the application configuration that reference our new controller and it’s not-yet-implemented actions.
Update /conf/routes with paths to the Widgets controller such that the following is true:
# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~

# Home page
GET        /                    controllers.Application.index
GET        /cleanup             controllers.Application.cleanup

#Widgets
GET        /api/widgets         controllers.Widgets.index
GET        /api/widget/:id      controllers.Widgets.read(id: String)
POST       /api/widget          controllers.Widgets.create
DELETE     /api/widget/:id      controllers.Widgets.delete(id: String)
PATCH      /api/widget/:id      controllers.Widgets.update(id: String)
Now, we can visit one of these new paths to view the default “TODO” screen.
Screen Shot 2016-01-25 at 10.18.35 PM

Data access

Before we can build out our controller actions, we’ll add a data access layer to surface some data to our controller.  Let’s start by creating the trait, which will define the contract for our data access layer.  (Traits are similar to Interfaces in Java – more info that here http://docs.scala-lang.org/tutorials/tour/traits.html)
Create a new folder called ‘repos’ such that a directory at ‘app/repos’ exists.  In that directory, create a new Scala file named WidgetRepo.scala with the following content:
app/repos/WidgetRepo.scala:
package repos

import javax.inject.Inject

import play.api.libs.json.{JsObject, Json}
import play.modules.reactivemongo.ReactiveMongoApi
import play.modules.reactivemongo.json._
import play.modules.reactivemongo.json.collection.JSONCollection
import reactivemongo.api.ReadPreference
import reactivemongo.api.commands.WriteResult
import reactivemongo.bson.{BSONDocument, BSONObjectID}

import scala.concurrent.{ExecutionContext, Future}

trait WidgetRepo {
  def find()(implicit ec: ExecutionContext): Future[List[JsObject]]

  def select(selector: BSONDocument)(implicit ec: ExecutionContext): Future[Option[JsObject]]

  def update(selector: BSONDocument, update: BSONDocument)(implicit ec: ExecutionContext): Future[WriteResult]

  def remove(document: BSONDocument)(implicit ec: ExecutionContext): Future[WriteResult]

  def save(document: BSONDocument)(implicit ec: ExecutionContext): Future[WriteResult]
}
There should be no surprises there – normal CRUD operations for data access and manipulation.  Things you may notice that are special here – the return types.  They’re all asynchronous Futures.  The read operations return JsObjects https://www.playframework.com/documentation/2.0/api/scala/play/api/libs/json/JsObject.html and the write operations return WriteResults http://reactivemongo.org/releases/0.11/documentation/tutorial/write-documents.html.
Let’s continue by adding the implementations for each of these methods in the form of a Scala class in the same source file:
app/repos/WidgetRepo.scala:
class WidgetRepoImpl @Inject() (reactiveMongoApi: ReactiveMongoApi) extends WidgetRepo {

  def collection = reactiveMongoApi.db.collection[JSONCollection]("widgets");

  override def find()(implicit ec: ExecutionContext): Future[List[JsObject]] = {
    val genericQueryBuilder = collection.find(Json.obj());
    val cursor = genericQueryBuilder.cursor[JsObject](ReadPreference.Primary);
    cursor.collect[List]()
  }

  override def select(selector: BSONDocument)(implicit ec: ExecutionContext): Future[Option[JsObject]] = {
    collection.find(selector).one[JsObject]
  }

  override def update(selector: BSONDocument, update: BSONDocument)(implicit ec: ExecutionContext): Future[WriteResult] = {
    collection.update(selector, update)
  }

  override def remove(document: BSONDocument)(implicit ec: ExecutionContext): Future[WriteResult] = {
    collection.remove(document)
  }

  override def save(document: BSONDocument)(implicit ec: ExecutionContext): Future[WriteResult] = {
    collection.update(BSONDocument("_id" -> document.get("_id").getOrElse(BSONObjectID.generate)), document, upsert = true)
  }

}

Again, there shouldn’t be much that’s surprising here other than the normal, somewhat complex IMO, Scala syntax.  You can see the implicit ExecutionContext, which, for asynchronous code, basically let’s Scala decide where in the thread pool to execute the related function.  You may also notice the ReadPreference in the find() function.  This tells Mongo that our repo would like to read it’s results from the primary Mongo node.

Back to the controller

At this point, we can return to our controller to round out the implementation details there.  Let’s start simple.
Before we can start adding implementation details, we need to configure some dependency injection details.  We’ll inject the ReactiveMongoApi into our Controller, then use that to create our repository.  Update the signature of the Widgets controller and imports so the following is true:
package controllers

import javax.inject.Inject

import play.api.libs.concurrent.Execution.Implicits.defaultContext
import play.api.libs.json.Json
import play.api.mvc._
import play.modules.reactivemongo.{MongoController, ReactiveMongoApi, ReactiveMongoComponents}
import reactivemongo.api.commands.WriteResult
import reactivemongo.bson.{BSONObjectID, BSONDocument}
import repos.WidgetRepoImpl

class Widgets @Inject()(val reactiveMongoApi: ReactiveMongoApi) extends Controller
    with MongoController with ReactiveMongoComponents {

  def widgetRepo = new WidgetRepoImpl(reactiveMongoApi)

In ‘app/controllers/Widgets.scala’, we have an unimplemented ‘index’ function.  This is meant to display a list of all widgets in the database, selected without parameters.  The implementation for this function is very straightforward. Update the index function such that:

  def index = Action.async { implicit request =>
    widgetRepo.find().map(widgets => Ok(Json.toJson(widgets)))
  }
In this implementation, since we’re expecting a single result, when we execute ‘map’ on the result, we’ll take the resulting object and render it directly to our JSON response.
The ‘read’ method, which is very similar to ‘index’, will take a single String parameter (the id of the document being searched for) and return a single result as JSON string. Update the read function such that:
  def read(id: String) = Action.async { implicit request =>
    widgetRepo.select(BSONDocument(Id -> BSONObjectID(id))).map(widget => Ok(Json.toJson(widget)))
  }

 The ‘delete’ method is similarly straightforward in that it takes a String id for the document to be deleted, and returns a HTTP status code 202 (Accepted), and no body.

  def delete(id: String) = Action.async {
    widgetRepo.remove(BSONDocument(Id -> BSONObjectID(id)))
        .map(result => Accepted)
  }

The ‘create’ and ‘update’ methods introduce a small amount of complexity in that they require the request body to be parsed using Scala’s built in pattern matching functionality. Since we’ll have to match the field names in two places, we’ll create a companion object to hold our field names. Create a ‘WidgetFields’ companion object in the Widgets controller source file:

object WidgetFields {
  val Id = "_id"
  val Name ="name"
  val Description = "description"
  val Author = "author"
}

In the body of the Widget controller, add a scoped import for the companion object:

import controllers.WidgetFields._

For the ‘create’ method, we’ll apply a JSON Body Parser to an implicit request, parsing out the relevant content needed to build a BSONDocument that can be persisted via the Repo:

def create = Action.async(BodyParsers.parse.json) { implicit request =>
    val name = (request.body \ Name).as[String]
    val description = (request.body \ Description).as[String]
    val author = (request.body \ Author).as[String]
    recipeRepo.save(BSONDocument(
      Name -> name,
      Description -> description,
      Author -> author
    )).map(result => Created)
  }

Execution of this method returns the HTTP status code 201.  For the ‘update’ method, we’ll perform largely the same operation – applying the JSON Body Parser to an implicit request, however, this time we’ll call a different repo method – this time building a BSONDocument to select the relevant document with, then passing in the current field values:

  def update(id: String) = Action.async(BodyParsers.parse.json) { implicit request =>
    val name = (request.body \ Name).as[String]
    val description = (request.body \ Description).as[String]
    val author = (request.body \ Author).as[String]
    widgetRepo.update(BSONDocument(Id -> BSONObjectID(id)),
      BSONDocument("$set" -> BSONDocument(Name -> name, Description -> description, Author -> author)))
        .map(result => Accepted)
  }

Testing!

Part 3 of this series will cover testing using the Spec2 library. In the mean time…We have a fully functioning REST API – but testing manually requires configuration and the execution of HTTP operations. Many web frameworks are packed with functionality that allows a developer to ‘bootstrap’ an application – adding seed data to a local environment for testing as an example. Recent changes in the Play! framework’s GlobalSettings have changed the way developers do things like seed test databases (https://www.playframework.com/documentation/2.4.x/GlobalSettings). While the dust settles, and while we wait for part 3, I created some helper functions in the Application controller that will create and remove some test data:

package controllers

import javax.inject.{Inject, Singleton}

import play.api.Logger
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import play.api.libs.json.Json
import play.api.mvc.{Action, Controller}
import play.modules.reactivemongo.json.collection.JSONCollection
import play.modules.reactivemongo.{MongoController, ReactiveMongoApi, ReactiveMongoComponents}
import reactivemongo.api.collections.bson.BSONCollection
import reactivemongo.api.commands.bson.BSONCountCommand.{ Count, CountResult }
import reactivemongo.api.commands.bson.BSONCountCommandImplicits._
import reactivemongo.bson.BSONDocument

import scala.concurrent.Future

@Singleton
class Application @Inject()(val reactiveMongoApi: ReactiveMongoApi) extends Controller
    with MongoController with ReactiveMongoComponents {

  def jsonCollection = reactiveMongoApi.db.collection[JSONCollection]("widgets");
  def bsonCollection = reactiveMongoApi.db.collection[BSONCollection]("widgets");

  def index = Action {
    Logger.info("Application startup...")

    val posts = List(
      Json.obj(
        "name" -> "Widget One",
        "description" -> "My first widget",
        "author" -> "Justin"
      ),
      Json.obj(
        "name" -> "Widget Two: The Return",
        "description" -> "My second widget",
        "author" -> "Justin"
      ))

    val query = BSONDocument("name" -> BSONDocument("$exists" -> true))
    val command = Count(query)
    val result: Future[CountResult] = bsonCollection.runCommand(command)

    result.map { res =>
      val numberOfDocs: Int = res.value
      if(numberOfDocs > 1) {
        jsonCollection.bulkInsert(posts.toStream, ordered = true).foreach(i => Logger.info("Record added."))
      }
    }

    Ok("Your database is ready.")
  }

  def cleanup = Action {
    jsonCollection.drop().onComplete {
      case _ => Logger.info("Database collection dropped")
    }
    Ok("Your database is clean.")
  }
}

… and routes …

# Home page
GET        /                    controllers.Application.index
GET        /cleanup             controllers.Application.cleanup

Advertisements

8 comments

  1. Also – there is no mention of how to turn off CORs errors here. “Response to preflight is invalid” even with the standard CORSfilter. Given that I don’t have the time to debug this, this is a ‘no-op’ in terms of getting a functional application off the ground, and I will have to look elsewhere. There may be a package in here that is broken maybe?

    1. Finally got a CORSfilter on here – and….still broken. Execution exception[[ConnectionNotInitialized: MongoError[‘Connection is missing metadata (like protocol version, etc.) The connection pool is probably being initialized.’]]] :::Doesn’t work, looking elsewhere.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s