play-api
play-api copied to clipboard
Play Framework standards for creating conventional applications
Play API
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
- Routes
- Controllers
- Serializers
- Services
- Data Access Objects
- Database Migrations
- Additional Files
- 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
Controllersuffix. E.g.PostsController. - Controllers should be a
classinstead 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 _)
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
Serializersuffix. E.g.PostSerializer. - Prefer creating a
new Writesinstance over using the play-jsonJson.formatmacro:
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
Servicesuffix. 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
DAOsuffix. 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.xmlas changelog names. - Tag your database after making each significant change for easy rollback.
Plugins
- Use Scoverage with
coverageMinimum := 100andcoverageFailOnMinimum := truein 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.
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 runand$ ./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
Specas the suffix for unit tests andIntegrationSpecas 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.