1. Overview
For building simple, CRUD-style REST APIs in Scala, the Play Framework is a good solution. It has an uncomplicated API that doesn’t require us to write too much code.
In this tutorial, we’re going to build a REST API in Scala with Play. We’ll use JSON as the data format and look at multiple HTTP methods and status codes.
2. The Example Project
2.1. What Are We Building?
As our example, we’ll build a todo list application. Rather than use a database, we’ll store the todo list items in memory.
The application will provide several endpoints, and we’ll build it in small, incremental steps.
We’ll also look at how to run and test our application as we grow it.
2.1. Setup the Project
First, let’s set up a new Play Framework project using an sbt template:
$ sbt new playframework/play-scala-seed.g8
This creates a new project with one controller (in the app/controllers directory), two HTML files (in the app/views directory), and a basic configuration (in the conf directory).
As we don’t need them, let’s remove HomeController.scala, index.scala.html, and main.scala.html files. Let’s also remove the existing content of the routes file.
3. The First REST Endpoint
Let’s begin by implementing an endpoint that returns the NoContent response.
3.1. Create a Controller
First, we create a new controller class in the app/controllers directory.
@Singleton
class TodoListController @Inject()(val controllerComponents: ControllerComponents)
extends BaseController {
}
The new class extends BaseController and has a constructor that’s compatible with it.
We’ve used the @Inject annotation to instruct the Play Framework to pass the required class dependencies automatically. And, we marked the class as a @Singleton so that the framework will create only one instance. This means it will reuse it for every request.
3.2. Handle the Request in Scala
Now we have a controller, let’s create the method that will be called when our server receives a REST request. First, we define a getAll function that returns a Play Framework Action:
def getAll(): Action[AnyContent] = Action {
NoContent
}
Action gives us access to the request parameters and can return an HTTP response. In this case, we’re just returning the NoContent status.
3.3. Add the Endpoint to Routes
Next, we have to add the controller to the routes file:
GET /todo controllers.TodoListController.getAll
We must specify the HTTP method we want to handle, the path, and the canonical name of the Scala method that handles the request. We need to separate them with whitespace.
By convention, we keep parameters in easily distinguishable columns to make the file readable.
3.4. Testing the REST Endpoint
Now we have an endpoint, let’s start the application:
$ sbt run
After a while, we’ll see the log message:
[info] p.c.s.AkkaHttpServer - Listening for HTTP on /0:0:0:0:0:0:0:0:9000
This means the application is ready to handle requests.
Now, we can test the API using curl. Let’s use curl‘s verbose mode:
$ curl -v localhost:9000/todo
GET /todo HTTP/1.1
Host: localhost:9000
User-Agent: curl/7.64.1
Accept: */*
HTTP/1.1 204 No Content.
Now we have a working Play Framework application, let’s add some features to it.
4. Returning Items
Now, we are going to return the whole todo list. Let’s define the data model, create an in-memory collection of tasks, and modify the TodoListController to return JSON objects.
4.1. Define the Model
First, we create a new class in the app/models directory:
case class TodoListItem(id: Long, description: String, isItDone: Boolean)
4.2. In-Memory List
Now, we define the list of tasks in the TodoListController class. As this list will be modified, we need the mutable collections package:
class TodoListController @Inject()(val controllerComponents: ControllerComponents)
extends BaseController {
private val todoList = new mutable.ListBuffer[TodoListItem]()
todoList += TodoListItem(1, "test", true)
todoList += TodoListItem(2, "some other value", false)
To help with our testing, we’ve added a couple of hard-coded values in this list to be available from startup.
4.3. JSON Formatter
Next, let’s create the JSON formatter that converts the TodoListItem object into JSON. We start by importing the JSON library:
import play.api.libs.json._
After that, we create the JSON formatter inside the TodoListController class:
implicit val todoListJson = Json.format[TodoListItem]
We make it an implicit field to avoid having to pass it to the Json.toJson function all the time.
4.4. Conditional Logic in the getAll Function
Now we have some data to return, let’s change our getAll function to return the NoContent status code only when the list is empty. Otherwise, it should return the list items converted to JSON:
def getAll(): Action[AnyContent] = Action {
if (todoList.isEmpty) {
NoContent
} else {
Ok(Json.toJson(todoList))
}
}
4.5. Testing
Let’s re-test the GET endpoint:
$ curl localhost:9000/todo
[
{
"id": 1,
"description": "test",
"isItDone": true
},
{
"id": 2,
"description": "some other value",
"isItDone": false
}
]
5. Returning One Item
A REST API should also support retrieving individual items via a path parameter. We want to be able to do something like:
$ curl localhost:9000/todo/1
This should return the item, or NotFound if the ID is unknown. Let’s implement that.
5.1. Adding a Parameter to the Route
First, we define a new endpoint in the routes file:
GET /todo/:itemId controllers.TodoListController.getById(itemId: Long)
The notation /todo/:itemId
means that the Play Framework should capture everything after the /todo/
prefix and assign it to the itemId
variable. After that, Play calls the getById
function and passes the itemId
as its first parameter.
Because we have specified the parameter type, it automatically converts the text to a number or returns a BadRequest if the parameter is not a number.
5.2. Finding the Matching Element
In the TodoListController, let’s add the getById method:
def getById(itemId: Long) = Action {
val foundItem = todoList.find(_.id == itemId)
foundItem match {
case Some(item) => Ok(Json.toJson(item))
case None => NotFound
}
}
Our method gets the itemId parameter and tries to find the todo list item with the same id.
We’re using the find function, which returns an instance of the Option class. So, we also use pattern matching to distinguish between an empty Option and an Option with a value.
When the item is present in the todo list, it’s converted into JSON returned in an OK response. Otherwise, NotFound is returned – causing an HTTP 404.
5.3. Testing
Now, we can try to get an item that is found:
$ curl localhost:9000/todo/1
{
"id": 1,
"description": "test",
"isItDone": true
}
Or an item that isn’t:
$ curl -v localhost:9000/todo/999
HTTP/1.1 204 No Content.
6. PUT and DELETE Methods
The Play Framework handles all HTTP methods, so when we want to use PUT or DELETE, we need to configure them in the routes file. For example, we can define endpoints that mark an item as completed and remove completed items:
PUT /todo/done/:itemId controllers.TodoListController.markAsDone(itemId: Long)
DELETE /todo/done controllers.TodoListController.deleteAllDone
7. Adding a New Task
Finally, our implementation should add a new task when we send a POST request containing a new item:
$ curl -v -d '{"description": "some new item"}' -H 'Content-Type: application/json' -X POST localhost:9000/todo
Note that we must specify only the description. Our application will generate the id and the initial status.
7.1. POST Endpoint in the routes File
First, let’s specify the new endpoint in the routes file:
POST /todo controllers.TodoListController.addNewItem
7.2. Data Transfer Object
Now, we need to add a new class to the app/models directory. We create a new data transfer object (DTO), which contains the description field:
case class NewTodoListItem(description: String)
7.3. Reading the JSON Object
In the TodoListController, we need a JSON formatter for that new class:
implicit val newTodoListJson = Json.format[NewTodoListItem]
Let’s also define a method to create NewTodoListItem objects from the JSON input:
def addNewItem() = Action { implicit request =>
val content = request.body
val jsonObject = content.asJson
val todoListItem: Option[NewTodoListItem] =
jsonObject.flatMap(
Json.fromJson[NewTodoListItem](_).asOpt
)
}
In our method, content.asJson
parses the given JSON object and returns an Option. We would get a valid object only if the deserialization were successful.
If the caller has sent us content that cannot be deserialized as a NewTodoListItem or Content-Type was not application/json, we end up with a None instead.
7.4. Adding a New Item
Now, let’s add the following code to the end of addNewItem. This will either store the new object and return HTTP Created or respond with BadRequest:
def addNewItem() = Action { implicit request =>
// existing code
todoListItem match {
case Some(newItem) =>
val nextId = todoList.map(_.id).max + 1
val toBeAdded = TodoListItem(nextId, newItem.description, false)
todoList += toBeAdded
Created(Json.toJson(toBeAdded))
case None =>
BadRequest
}
}
7.5. Testing
Let’s test adding a new item to the list:
$ curl -v -d '{"description": "some new item"}' -H 'Content-Type: application/json' -X POST localhost:9000/todo
HTTP/1.1 201 Created.
{
"id": 3,
"description": "some new item",
"isItDone": false
}
In response, we should see the Created status code and the new item in the response body. Additionally, to verify that the item was added to the list, we can retrieve all items again:
$ curl localhost:9000/todo
[
{
"id": 1,
"description": "test",
"isItDone": true
},
{
"id": 2,
"description": "some other value",
"isItDone": false
},
{
"id": 3,
"description": "some new item",
"isItDone": false
}
]
This time, the JSON array should contain three objects, including the newly added one.
We should note that it’s crucial to specify the Content-Type header. Otherwise, Play Framework reads the data as application/x-www-form-urlencoded
and fails to convert it into a JSON object.
8. Conclusion
In this article, we implemented a REST API in the Play Framework using Scala.
First, we initialized the project and defined our first route and controller class. Then we defined DTO objects and converted them in and out of JSON format.
We have also looked at how to use curl to verify that our code works correctly.
The example source code is available over on GitHub.