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

Alex Merced - Oct 1 '23 - - Dev Community

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"
)
Enter fullscreen mode Exit fullscreen mode

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

addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.8")
Enter fullscreen mode Exit fullscreen mode

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

sbt.version=1.5.5
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .