DEV Community

Alex Merced
Alex Merced

Posted on

1

How to write a JSON API with Scala and 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"
)
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.

Heroku

Simplify your DevOps and maximize your time.

Since 2007, Heroku has been the go-to platform for developers as it monitors uptime, performance, and infrastructure concerns, allowing you to focus on writing code.

Learn More

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay