DEV Community

Alex Merced
Alex Merced

Posted on

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.

Top comments (0)