La interfaz está localizada; las descripciones y documentos de las Skills se traducen automáticamente con IA a su idioma. El código, los comandos, rutas y términos técnicos se mantienen en el original, y prevalece el documento del autor.
Explorar Skills
Datos e investigación · SKILL.md

scala-play

scala-play

Use this skill when building Scala Play Framework applications — MVC architecture, Slick database access, JSON handling with Play JSON, async actions, and testing. This skill enforces: thin controllers, service layer, Slick for database, Play JSON (reads/writes) for serialization, Future-based async actions. Requires Scala 2.13+ or 3 and Play Framework 3.x.…

Independent GitHub snapshot · Automated classification · Original documentation governs

Documentación de la Skill

GitHub

name: scala-play description: > Use this skill when building Scala Play Framework applications — MVC architecture, Slick database access, JSON handling with Play JSON, async actions, and testing. This skill enforces: thin controllers, service layer, Slick for database, Play JSON (reads/writes) for serialization, Future-based async actions. Requires Scala 2.13+ or 3 and Play Framework 3.x. Do NOT use for: Akka HTTP, http4s, ZIO, or non-Play Scala backends. version: "1.0.0" author: "j4flmao" license: "MIT" compatibility: claude-code: true cursor: true codex: true windsurf: true tags: [backend, scala, play, phase-7]

Scala Play Framework

Purpose

Build Play Framework applications with clean MVC architecture, Slick database access, Play JSON serialization, async actions, and comprehensive testing.

Agent Protocol

Trigger

User request includes: Play Framework, Play Scala, Play controller, Play Slick, Play JSON, Play action, Play test, Play routes.

Input Context

  • Scala version (2.13, 3)
  • Play version (3.x)
  • Database (PostgreSQL via Slick)
  • JSON library (Play JSON, Circe)
  • Testing (ScalaTest, Specs2)

Output Artifact

Controller, model, Slick table definition, JSON reads/writes, routes, test.

Response Format

Produce artifact directly. No preamble, no postamble, no explanations.

Completion Criteria

  • Controller action returns Future[Result]
  • Slick table mapped to case class
  • JSON Format (Reads/Writes/OFormat) defined
  • Routes file with HTTP method binding
  • Service layer for business logic

Max Response Length

4096 tokens

Architecture Decision Trees

Play vs Akka HTTP vs http4s vs ZIO HTTP

CriterionPlayAkka HTTPhttp4sZIO HTTP
EcosystemFull (MVC, Slick, JSON)HTTP + Akka ecosystemFunctional + CatsZIO ecosystem
AsyncFuture-basedAkka StreamsIO/Cats EffectZIO
Type levelModerateModerateHigh (functional)High
Learning curveLow-ModerateModerateHigh (FP concepts)High (ZIO)
CommunityLarge (Scala standard)MediumMediumGrowing

Decision: Full framework (MVC, DB, JSON) → Play. Lightweight HTTP → http4s. Streaming/Akka → Akka HTTP. ZIO stack → ZIO HTTP.

Slick vs Doobie vs Quill

CriterionSlickDoobieQuill
ApproachScala query DSLFree monad (SQL strings)Macro-based Quill DSL
Type safetyHighHighHigh
SQL controlMedium (DSL maps to SQL)Full (raw SQL)Medium (Quill DSL)
MigrationPlay Evolutions + FlywayFlyway/LiquibaseManual
Learning curveModerateHigh (Free monad)Moderate

Decision: Play integration → Slick. Raw SQL / JDBC → Doobie. Compile-time query generation → Quill.

Workflow

Step 1: Project Structure (Play)

app/
  controllers/
    UserController.scala
    OrderController.scala
  models/
    User.scala
    Order.scala
  services/
    UserService.scala
    OrderService.scala
  repositories/
    UserRepository.scala
  views/
    (not in API mode)
  dto/
    CreateUserRequest.scala
    UserResponse.scala
  error/
    ErrorHandler.scala
    AppError.scala
conf/
  routes
  application.conf
  evolutions/
    default/
      1.sql
project/
  build.properties
  plugins.sbt
build.sbt
test/
  controllers/
    UserControllerSpec.scala
  services/
    UserServiceSpec.scala

Step 2: build.sbt

name := "my-api"
version := "1.0.0"
scalaVersion := "2.13.12"

lazy val root = (project in file("."))
  .enablePlugins(PlayScala)
  .settings(
    libraryDependencies ++= Seq(
      guice,
      "org.playframework" %% "play-slick" % "6.0.0",
      "org.playframework" %% "play-slick-evolutions" % "6.0.0",
      "org.postgresql" % "postgresql" % "42.7.0",
      "org.mindrot" % "jbcrypt" % "0.4",
      "com.github.jwt-scala" %% "jwt-play" % "9.4.0",
      "org.scalatestplus.play" %% "scalatestplus-play" % "7.0.0" % Test,
    )
  )

Step 3: Model and Slick Table

// app/models/User.scala
package models

import java.time.Instant
import java.util.UUID

case class User(
  id: UUID,
  email: String,
  name: String,
  role: String,
  active: Boolean,
  createdAt: Instant,
)

// app/repositories/UserRepository.scala
package repositories

import javax.inject.{Inject, Singleton}
import models.User
import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider}
import slick.jdbc.JdbcProfile
import slick.lifted.ProvenShape
import java.time.Instant
import java.util.UUID
import scala.concurrent.Future

@Singleton
class UserRepository @Inject()(
  protected val dbConfigProvider: DatabaseConfigProvider
) extends HasDatabaseConfigProvider[JdbcProfile] {

  import profile.api._

  class UserTable(tag: Tag) extends Table[User](tag, "users") {
    def id = column[UUID]("id", O.PrimaryKey)
    def email = column[String]("email", O.Unique)
    def name = column[String]("name")
    def role = column[String]("role", O.Default("user"))
    def active = column[Boolean]("active", O.Default(true))
    def createdAt = column[Instant]("created_at")
    def * : ProvenShape[User] = (id, email, name, role, active, createdAt) <> (User.tupled, User.unapply)
  }

  val users = TableQuery[UserTable]

  def findById(id: UUID): Future[Option[User]] =
    db.run(users.filter(_.id === id).result.headOption)

  def findByEmail(email: String): Future[Option[User]] =
    db.run(users.filter(_.email === email).result.headOption)

  def findAll(page: Int, pageSize: Int): Future[Seq[User]] =
    db.run(users.sortBy(_.createdAt.desc).drop((page - 1) * pageSize).take(pageSize).result)

  def count(): Future[Int] =
    db.run(users.length.result)

  def save(user: User): Future[Int] =
    db.run(users += user)

  def update(id: UUID, user: User): Future[Int] =
    db.run(users.filter(_.id === id).update(user))

  def delete(id: UUID): Future[Int] =
    db.run(users.filter(_.id === id).delete)
}

Step 4: JSON Reads/Writes (DTO)

// app/dto/CreateUserRequest.scala
package dto

import play.api.libs.json.{Json, Reads}

case class CreateUserRequest(
  email: String,
  name: String,
  role: Option[String],
)

object CreateUserRequest {
  implicit val reads: Reads[CreateUserRequest] = Json.reads[CreateUserRequest]
}

// app/dto/UserResponse.scala
package dto

import models.User
import play.api.libs.json.{Json, OWrites, Writes}
import java.time.Instant
import java.util.UUID

case class UserResponse(
  id: UUID,
  email: String,
  name: String,
  role: String,
  active: Boolean,
  createdAt: Instant,
)

object UserResponse {
  implicit val writes: OWrites[UserResponse] = Json.writes[UserResponse]

  def from(user: User): UserResponse =
    UserResponse(user.id, user.email, user.name, user.role, user.active, user.createdAt)
}

// app/dto/PaginatedResponse.scala
package dto

import play.api.libs.json.{Json, OWrites}

case class PaginatedResponse[T](
  data: Seq[T],
  total: Int,
  page: Int,
  pageSize: Int,
)

object PaginatedResponse {
  implicit def writes[T](implicit tWrites: OWrites[T]): OWrites[PaginatedResponse[T]] =
    Json.writes[PaginatedResponse[T]]
}

Step 5: Service Layer

// app/services/UserService.scala
package services

import dto.{CreateUserRequest, UserResponse}
import models.User
import repositories.UserRepository
import org.mindrot.jbcrypt.BCrypt

import javax.inject.{Inject, Singleton}
import scala.concurrent.{ExecutionContext, Future}
import java.time.Instant
import java.util.UUID

@Singleton
class UserService @Inject()(
  userRepository: UserRepository,
)(implicit ec: ExecutionContext) {

  def create(request: CreateUserRequest): Future[UserResponse] =
    userRepository.findByEmail(request.email).flatMap {
      case Some(_) =>
        Future.failed(new AppError.Conflict("Email already exists"))
      case None =>
        val user = User(
          id = UUID.randomUUID(),
          email = request.email.toLowerCase,
          name = request.name,
          role = request.role.getOrElse("user"),
          active = true,
          createdAt = Instant.now,
        )
        userRepository.save(user).map(_ => UserResponse.from(user))
    }

  def findById(id: UUID): Future[UserResponse] =
    userRepository.findById(id).flatMap {
      case Some(user) => Future.successful(UserResponse.from(user))
      case None => Future.failed(new AppError.NotFound("User"))
    }

  def findAll(page: Int, pageSize: Int): Future[PaginatedResponse[UserResponse]] =
    for {
      users <- userRepository.findAll(page, pageSize)
      total <- userRepository.count()
    } yield PaginatedResponse(
      data = users.map(UserResponse.from),
      total = total,
      page = page,
      pageSize = pageSize,
    )

  def delete(id: UUID): Future[Unit] =
    userRepository.delete(id).map {
      case 1 => ()
      case 0 => throw new AppError.NotFound("User")
    }
}

Step 6: Error Handling

// app/error/AppError.scala
package error

sealed trait AppError extends Exception with Product with Serializable {
  def statusCode: Int
  def code: String
  def message: String
}

object AppError {
  case class NotFound(entity: String)
    extends RuntimeException(s"$entity not found") with AppError {
    val statusCode = 404
    val code = "NOT_FOUND"
    val message = s"$entity not found"
  }
  case class Conflict(msg: String)
    extends RuntimeException(msg) with AppError {
    val statusCode = 409
    val code = "CONFLICT"
    val message = msg
  }
  case class Validation(details: Seq[(String, String)])
    extends RuntimeException("Validation error") with AppError {
    val statusCode = 422
    val code = "VALIDATION_ERROR"
    val message = "Validation error"
  }
}

// app/error/ErrorHandler.scala
package error

import play.api.http.HttpErrorHandler
import play.api.http.Status._
import play.api.libs.json.Json
import play.api.mvc.{RequestHeader, Result, Results}

import javax.inject.Singleton
import scala.concurrent.Future

@Singleton
class ErrorHandler extends HttpErrorHandler {
  def onClientError(request: RequestHeader, statusCode: Int, message: String): Future[Result] =
    Future.successful {
      Results.Status(statusCode)(Json.obj(
        "error" -> Json.obj("code" -> "CLIENT_ERROR", "message" -> message)
      ))
    }

  def onServerError(request: RequestHeader, exception: Throwable): Future[Result] = {
    val (status, body) = exception match {
      case e: AppError =>
        e.statusCode -> Json.obj("error" -> Json.obj("code" -> e.code, "message" -> e.message))
      case _ =>
        INTERNAL_SERVER_ERROR -> Json.obj("error" -> Json.obj("code" -> "INTERNAL_ERROR", "message" -> "Server error"))
    }
    Future.successful(Results.Status(status)(body))
  }
}

Step 7: Controller

// app/controllers/UserController.scala
package controllers

import dto.{CreateUserRequest, UserResponse}
import error.AppError
import play.api.libs.json.{JsError, Json}
import play.api.mvc.{AbstractController, Action, AnyContent, ControllerComponents}
import services.UserService

import javax.inject.{Inject, Singleton}
import scala.concurrent.{ExecutionContext, Future}
import java.util.UUID

@Singleton
class UserController @Inject()(
  cc: ControllerComponents,
  userService: UserService,
)(implicit ec: ExecutionContext) extends AbstractController(cc) {

  def list(page: Int, pageSize: Int): Action[AnyContent] = Action.async {
    userService.findAll(page, pageSize).map { result =>
      Ok(Json.toJson(result))
    }.recover(errorHandler)
  }

  def getById(id: UUID): Action[AnyContent] = Action.async {
    userService.findById(id).map { user =>
      Ok(Json.toJson(user))
    }.recover(errorHandler)
  }

  def create: Action[AnyContent] = Action.async { request =>
    request.body.asJson.map { json =>
      json.validate[CreateUserRequest].fold(
        errors => Future.successful(BadRequest(JsError.toJson(errors))),
        createReq => userService.create(createReq).map { user =>
          Created(Json.toJson(user))
        }.recover(errorHandler)
      )
    }.getOrElse(Future.successful(BadRequest(Json.obj("error" -> "Invalid JSON"))))
  }

  def delete(id: UUID): Action[AnyContent] = Action.async {
    userService.delete(id).map { _ =>
      NoContent
    }.recover(errorHandler)
  }

  private val errorHandler: PartialFunction[Throwable, play.api.mvc.Result] = {
    case AppError.NotFound(entity) => NotFound(Json.obj("error" -> s"$entity not found"))
    case AppError.Conflict(msg)    => Conflict(Json.obj("error" -> msg))
    case e                         => InternalServerError(Json.obj("error" -> "Server error"))
  }
}

Step 8: Routes

# conf/routes
GET     /api/v1/users                   controllers.UserController.list(page: Int ?= 1, pageSize: Int ?= 20)
GET     /api/v1/users/:id               controllers.UserController.getById(id: java.util.UUID)
POST    /api/v1/users                   controllers.UserController.create
DELETE  /api/v1/users/:id               controllers.UserController.delete(id: java.util.UUID)

Production Considerations

Configuration

# conf/application.conf
play.http.errorHandler = "error.ErrorHandler"
play.filters.hosts.allowed = [".example.com"]
play.filters.headers.contentSecurityPolicy = "default-src 'self'"

slick.dbs.default {
  profile = "slick.jdbc.PostgresProfilequot;
  db {
    driver = "org.postgresql.Driver"
    url = ${?DATABASE_URL}
    user = ${?DB_USER}
    password = ${?DB_PASS}
    numThreads = 10
    maxConnections = 20
    connectionPool = HikariCP
  }
}

Thread Pool Configuration

# Thread pool for DB operations (Slick uses application context)
play.akka {
  actor-system = "my-system"
  default-dispatcher {
    fork-join-executor {
      parallelism-factor = 3.0
      parallelism-max = 24
    }
  }
}

Anti-Patterns

Anti-PatternWhyFix
Blocking DB access in ActionBlocks Play's small thread poolAlways return Future[Result]
Business logic in controllerUntestable, violates MVCService layer
No error handling in actions500 on any failureRecover with error handler
Global JSON formattersImplicit conflictsPer-DTO implicit val
Await.result in controllerBlocks thread, deadlock riskReturn Future[Result]
String IDsPerformance, type safetyUUID or Long IDs

Security Considerations

  • Play's AllowedHostsFilter and CSPFilter enabled by default
  • CSRF protection for session-based auth; play.filters.csrf.CSRFFilter
  • SQL injection: Slick uses parameterized queries — safe by default
  • JSON validation with Reads — rejects malformed input
  • JWT with jwt-play — validate signature, expiration, issuer
  • CORS: play.filters.cors.CORSFilter with explicit origins

Testing Strategies

// test/controllers/UserControllerSpec.scala
class UserControllerSpec extends PlaySpec with GuiceOneAppPerTest with Injecting {
  "UserController" should {
    "return 201 on create" in {
      val request = FakeRequest(POST, "/api/v1/users")
        .withJsonBody(Json.obj("email" -> "test@test.com", "name" -> "Test"))
      val result = route(app, request).get
      status(result) mustBe CREATED
    }

    "return 200 on list" in {
      val request = FakeRequest(GET, "/api/v1/users")
      val result = route(app, request).get
      status(result) mustBe OK
    }
  }
}

Use ScalaTest + PlaySpec for controller tests. Use Mockito or ScalaMock for service mocking. Use scalacheck for property-based testing.

Rules

  • All controller actions return Future[Result] — never Action { ... } blocking.
  • Slick queries in repositories, wrapped with Future — never raw db.run in controllers.
  • JSON serialization via play.api.libs.json — Reads for input, Writes/OWrites for output.
  • Service layer for business logic — controllers call services, services call repositories.
  • Error handling via AppError sealed trait + HttpErrorHandler.
  • Guice DI via @Inject() and @Singleton — Play's default DI container.
  • Routes file is the API contract — one line per endpoint with type-safe parameters.

References

  • references/play-architecture.md — Play Architecture
  • references/play-framework-rest-api.md — REST API with Play
  • references/play-performance.md — Performance
  • references/play-rest-api.md — Play REST API Patterns
  • references/play-security.md — Play Security
  • references/play-setup.md — Setup Guide
  • references/play-testing.md — Testing Play
  • references/scala-concurrent-streaming.md — Concurrency and Streaming

Handoff

Hand off to backend/universal/api-response/SKILL.md for API response formatting or backend/universal/backend-testing/SKILL.md for test patterns.

Skills guardadas