/play-api

Play Framework standards for creating conventional applications

Primary LanguageShellMIT LicenseMIT

Play API

Build Status Codacy Badge

This is an example of how to build a Play Framework application with specific conventions for quick API development. The conventions are intended to make onboarding new developers as easy as possible while keeping the code as expressive and free of boilerplate as possible.

Conventions

  1. Routes
  2. Controllers
  3. Serializers
  4. Services
  5. Data Access Objects
  6. Database Migrations
  7. Additional Files
  8. Tests

Routes

  • Use URL based API versioning. E.g. /v1/posts.
  • Use 5 standard endpoints with appropriate HTTP verb:
GET       /v1/posts        # => Index
POST      /v1/posts        # => Create
GET       /v1/posts/:id    # => Show
PUT       /v1/posts/:id    # => Update
DELETE    /v1/posts/:id    # => Destroy

Controllers

  • Keep your controllers as simple as possible.
  • Namespace your controller's package with company and version. E.g. com.example.api.controllers.v1.
  • Use pluralized resource name with Controller suffix. E.g. PostsController.
  • Controllers should be a class instead of a global object.
  • Use the ControllerConventions file to abstract common controller code like model parsing.
  • Move repetitive error handling logic into ErrorHandler.
  • Import your model's JSON writes function from your corresponding serializer.
  • Include your model's JSON reads function in your controller for versioning, custom validations, and API clarity:
implicit val postJsonReads = (
  (__ \ "id").read[UUID] and
  (__ \ "title").read[String] and
  ...
)(Post.apply _)

Serializers

  • Create one serializer per model.
  • Namespace your serializers package with company and version. E.g. com.example.api.serializers.v1.
  • User the singularized resource name with Serializer suffix. E.g. PostSerializer.
  • Prefer creating a new Writes instance over using the play-json Json.format macro:
implicit val postJsonWrites = new Writes[Post] {
  def writes(post: Post): JsObject = Json.obj(
    "id" -> post.id,
    "title" -> post.title,
    ...
  )
}

Services

  • User the singularized resource name with Service suffix. E.g. PostService.
  • Keep business logic in the service layer as much as possible.
  • Use dependency injection to keep services decoupled.

Data Access Objects

  • User the singularized resource name with DAO suffix. E.g. PostDAO.
  • Create one DAO per database table or remote resource.
  • Move repetitive create, read, update, delete functions into shared DAOConventions.
  • Keep database or remote resource specifics in the DAO layer as much as possible.

Database Migrations

  • Use environment variables for database connection configuration in application.conf.
  • Use Liquibase to perform migrations and Play Liquibase to have migrations run in dev / test mode.
  • Use YYYMMDDHHMMSS_database_migration_description.xml as changelog names.
  • Tag your database after making each significant change for easy rollback.

Plugins

Additional Files

  • Include a .java-version file with the expected Java version.
  • Include a activator wrapper file that downloads all necessary dependencies (serving the app and testing the app should be as simple as: $ ./activator run and $ ./activator test).
  • Include a resetdb script that drops and re-creates the database for testing and new developers.

Tests

  • Prefer ScalaTest over Specs2.
  • Include integration tests and unit tests in the same package as the source files.
  • Use Spec as the suffix for unit tests and IntegrationSpec as the suffix for integration tests.
  • Reset the DB before each integration test with DatabaseCleaner to avoid order-dependant tests.
  • Prefer a real database over an in-memory stand in for integration tests to find DB specific bugs.
  • Use factories like the PostFactory to keep test setup DRY and expressive.
  • Prefer high level integration tests for common paths and unit tests for edge cases and full coverage.