Alex Merced's Data, Dev and AI Blog

Article

How to write a JSON API in Scala with Play from scratch

Step 1: Create a Blank SBT Project#

Create a new directory for your project and navigate to it in your terminal.

Create a build.sbt file in the project directory and add the following content:

name := "todo-api"

version := "1.0"

scalaVersion := "2.13.8"

libraryDependencies ++= Seq(
  "com.typesafe.play" %% "play-json" % "2.9.4",
  "com.typesafe.play" %% "play-slick" % "5.0.0",
  "com.typesafe.play" %% "play-slick-evolutions" % "5.0.0",
  "org.postgresql" % "postgresql" % "42.2.14",
  "com.typesafe.play" %% "play-guice" % "2.8.8",
  "com.typesafe.play" %% "play" % "2.8.8"
)

Create a project/plugins.sbt file with the following content:

addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.8")

Create a project/build.properties file with the following content:

sbt.version=1.5.5

Once we're done with this tutorial your file structure should look like this:

todo-api/
├── app/
│   ├── controllers/
│   │   └── TodoController.scala
│   ├── models/
│   │   └── Todo.scala
├── conf/
│   ├── application.conf
│   └── routes
├── project/
│   ├── build.properties
│   └── plugins.sbt
├── .gitignore
└── build.sbt

Step 2: Configure the Database#

Create a PostgreSQL database and remember the connection details (URL, username, and password).

Open the conf/application.conf file and configure the database connection:

db.default.driver = org.postgresql.Driver
db.default.url = "jdbc:postgresql://localhost:5432/your_database_name"
db.default.username = your_username
db.default.password = "your_password"

Step 3: Define the Model#

Create a models package in your project's app directory.

Inside the models package, create a Todo.scala file to define the "TODO" model:

package models

import play.api.libs.json._

case class Todo(id: Option[Long], task: String, completed: Boolean)

object Todo {
  implicit val todoFormat: OFormat[Todo] = Json.format[Todo]
}

Step 4: Create the Controller#

Create a controllers package in your project's app directory.

Inside the controllers package, create a TodoController.scala file to implement the API endpoints:

package controllers

import javax.inject.Inject
import play.api.libs.json._
import play.api.mvc._
import models.Todo
import play.api.db.slick.DatabaseConfigProvider
import slick.jdbc.JdbcProfile

import scala.concurrent.{ExecutionContext, Future}

class TodoController @Inject()(
    dbConfigProvider: DatabaseConfigProvider,
    cc: ControllerComponents
)(implicit ec: ExecutionContext)
    extends AbstractController(cc) {

  import profile.api._

  private val dbConfig = dbConfigProvider.get[JdbcProfile]

  private class TodoTable(tag: Tag) extends Table[Todo](tag, "todos") {
    def id = column[Option[Long]]("id", O.PrimaryKey, O.AutoInc)
    def task = column[String]("task")
    def completed = column[Boolean]("completed")

    def * = (id, task, completed) <> ((Todo.apply _).tupled, Todo.unapply)
  }

  private val todos = TableQuery[TodoTable]

  def createTodo: Action[JsValue] = Action.async(parse.json) { implicit request =>
    val todoResult = request.body.validate[Todo]
    todoResult.fold(
      errors => {
        Future(BadRequest(Json.obj("message" -> JsError.toJson(errors))))
      },
      todo => {
        val insertQuery = (todos returning todos.map(_.id)) += todo
        dbConfig.db.run(insertQuery).map { id =>
          Created(Json.obj("id" -> id, "task" -> todo.task, "completed" -> todo.completed))
        }
      }
    )
  }

  def getAllTodos: Action[AnyContent] = Action.async { _ =>
    val query = todos.result
    dbConfig.db.run(query).map { todos =>
      Ok(Json.toJson(todos))
    }
  }

  def getTodoById(id: Long): Action[AnyContent] = Action.async { _ =>
    val query = todos.filter(_.id === Some(id)).result.headOption
    dbConfig.db.run(query).map {
      case Some(todo) => Ok(Json.toJson(todo))
      case None => NotFound
    }
  }

  def updateTodo(id: Long): Action[JsValue] = Action.async(parse.json) { implicit request =>
    val todoResult = request.body.validate[Todo]
    todoResult.fold(
      errors => {
        Future(BadRequest(Json.obj("message" -> JsError.toJson(errors))))
      },
      updatedTodo => {
        val updateQuery = todos.filter(_.id === Some(id)).map(todo => (todo.task, todo.completed)).update((updatedTodo.task, updatedTodo.completed))
        dbConfig.db.run(updateQuery).map {
          case 0 => NotFound
          case _ => Ok(Json.toJson(updatedTodo))
        }
      }
    )
  }

  def deleteTodoById(id: Long): Action[AnyContent] = Action.async { _ =>
    val deleteQuery = todos.filter(_.id === Some(id)).delete
    dbConfig.db.run(deleteQuery).map {
      case 0 => NotFound
      case _ => NoContent
    }
  }
}

In this code:

The TodoController class defines endpoints for creating, retrieving, updating, and deleting TODO items.

It uses Play Framework's JSON serialization/deserialization to work with JSON data. It uses Slick for database interactions.

Step 5: Configure Routes#

Create a conf/routes file in your project's conf directory if it doesn't already exist.

Inside the routes file, define the routes for your API endpoints. Here's an example for the TodoController:

# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~

# Home page
GET     /                           controllers.HomeController.index

# Define routes for the TodoController
GET     /todos                      controllers.TodoController.getAllTodos
POST    /todos                      controllers.TodoController.createTodo
GET     /todos/:id                  controllers.TodoController.getTodoById(id: Long)
PUT     /todos/:id                  controllers.TodoController.updateTodo(id: Long)
DELETE  /todos/:id                  controllers.TodoController.deleteTodoById(id: Long)

In the routes file:

You define routes for various HTTP methods (GET, POST, PUT, DELETE) and associate them with controller methods in the format controllers.ControllerName.methodName(arguments).

For example, GET /todos maps to the getAllTodos method in the TodoController. The :id syntax in routes indicates a dynamic parameter that will be passed to the controller method.

By configuring the routes file, you specify how incoming requests are routed to the appropriate controller actions. This file is a crucial part of your Play Framework application's configuration.

Step 6: Run the Application#

Start the Play Framework application by running the following command in your project directory:

sbt run

Access the API endpoints at http://localhost:9000/todos.

You've now created a CRUD JSON API in Scala using the Play Framework with PostgreSQL integration. You can test your API using tools like Postman or curl.

Newsletter

Get new posts in your inbox

Deep dives on Apache Iceberg, lakehouse architecture and applied AI. No spam, unsubscribe anytime.

Subscribe

Menu

Search

Type at least two characters.