scalaintermediate

Play Framework REST Controller

Build REST APIs with Play Framework: routes, JSON serialization, async actions, and error handling.

scala
package controllers

import javax.inject.*
import play.api.mvc.*
import play.api.libs.json.*
import scala.concurrent.{ExecutionContext, Future}

case class Todo(id: Long, title: String, completed: Boolean)

object Todo:
  given Format[Todo] = Json.format[Todo]

@Singleton
class TodoController @Inject()(
  val controllerComponents: ControllerComponents
)(using ec: ExecutionContext) extends BaseController:

  // In-memory store
  private var todos = List(
    Todo(1, "Learn Scala", false),
    Todo(2, "Build API", false),
    Todo(3, "Deploy app", false)
  )
  private var nextId = 4L

  // GET /todos
  def list(): Action[AnyContent] = Action { implicit request =>
    Ok(Json.toJson(todos))
  }

  // GET /todos/:id
  def get(id: Long): Action[AnyContent] = Action {
    todos.find(_.id == id) match
      case Some(todo) => Ok(Json.toJson(todo))
      case None => NotFound(Json.obj("error" -> s"Todo $id not found"))
  }

  // POST /todos
  def create(): Action[JsValue] = Action(parse.json) { request =>
    request.body.validate[Todo].fold(
      errors => BadRequest(Json.obj("error" -> JsError.toJson(errors))),
      todo =>
        val newTodo = todo.copy(id = nextId)
        nextId += 1
        todos = todos :+ newTodo
        Created(Json.toJson(newTodo))
    )
  }

  // PUT /todos/:id
  def update(id: Long): Action[JsValue] = Action(parse.json) { request =>
    request.body.validate[Todo].fold(
      errors => BadRequest(Json.obj("error" -> JsError.toJson(errors))),
      updated =>
        todos.find(_.id == id) match
          case Some(_) =>
            todos = todos.map(t => if t.id == id then updated.copy(id = id) else t)
            Ok(Json.toJson(updated.copy(id = id)))
          case None =>
            NotFound(Json.obj("error" -> s"Todo $id not found"))
    )
  }

  // DELETE /todos/:id
  def delete(id: Long): Action[AnyContent] = Action {
    todos.find(_.id == id) match
      case Some(_) =>
        todos = todos.filterNot(_.id == id)
        NoContent
      case None =>
        NotFound(Json.obj("error" -> s"Todo $id not found"))
  }

  // Async action
  def asyncGet(id: Long): Action[AnyContent] = Action.async {
    Future {
      todos.find(_.id == id) match
        case Some(todo) => Ok(Json.toJson(todo))
        case None => NotFound(Json.obj("error" -> s"Todo $id not found"))
    }
  }

// conf/routes:
// GET     /todos          controllers.TodoController.list()
// GET     /todos/:id      controllers.TodoController.get(id: Long)
// POST    /todos          controllers.TodoController.create()
// PUT     /todos/:id      controllers.TodoController.update(id: Long)
// DELETE  /todos/:id      controllers.TodoController.delete(id: Long)

Sponsored

Railway

Use Cases

  • REST API development
  • CRUD operations with Play
  • JSON API endpoints

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.