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.
- Routes
- Controllers
- Serializers
- Services
- Data Access Objects
- Database Migrations
- Additional Files
- Tests
- 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
- 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 globalobject
. - 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 _)
- 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-jsonJson.format
macro:
implicit val postJsonWrites = new Writes[Post] {
def writes(post: Post): JsObject = Json.obj(
"id" -> post.id,
"title" -> post.title,
...
)
}
- 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.
- 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.
- 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.
- Use Scoverage with
coverageMinimum := 100
andcoverageFailOnMinimum := true
in build.sbt. - Use Scalastyle with
level="error"
in scalastyle-config.xml. - Use Scalariform for standard style enforcement.
- Use a server monitoring tool like New Relic with sbt-newrelic.
- Enforce UTC timezone in JVM with sbt-utc.
- 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.
- 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 andIntegrationSpec
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.