An Omakase-style PlayFramework Template: PlayFast

I'm excited to share my opinionated PlayFramework Template, PlayFast. It has the needed components and code conventions that make you productive and ready for production. It would have taken days to weeks (depending on the expertise level), with a large amount of googling, to set up these components and code conventions.

With this template, you should be able to clone, run locally, run tests, be productive, be production-ready, and deploy within minutes!

The term Omakase is inspired from: https://dhh.dk/2012/rails-is-omakase.html  – In my previous life, I used Rails heavily before moving to PlayFramework.

Here are the 3 main components served to you Omakase-style:

1: Modern frontend framework integration (only Svelte + TailwindCSS for now)

Integrating a modern frontend framework like React, Vue, or Svelte is one of the hardest challenges to get started on PlayFramework. In order to make it work seamlessly, you will need to wrangle with SBT. That is exactly what I did.

I've built sbt-svelte that integrates with PlayFramework. It supports Hot-Reload Module (HMR) in development and, in production, bundling JS code using Webpack. The developer's experience is slick. For development, you simply run sbt run and npm run hmr; things will work and hot-reload as expected. For production, you simply run sbt stage docker:publish.

PlayFast proposes a convention of how you structure your JS code (or TS code). The approach is the middle ground between SPA and non-SPA. It doesn't do chunking. It only depends on Play's routing (no redundant JS routing). However, you can still implement JS routes within each page (I often don't do this). This approach scales very well from a small app to a highly complex app.

2: Testing

I'm a big fan of testing, particularly the ME2E testing style aka "Mostly End To End". In practice, this means browser testing with as fewer mocks as possible.

PlayFast comes with a set of example tests where browser testing has already been set up. The cookies and database are reset before every test.

WSClient's mock has also been set up where you can choose to mock only the endpoints you need, and the endpoints that you don't mock will be executed against the external services.

While I like my tests to hit the external services directly, other people might prefer more mocking than less, and this WSClient's mock allows that.

πŸ’‘
Regarding external services, I generally opt for calling their REST APIs directly through WSClient and avoid using their libraries that merely wrap around their APIs like stripe-java. It's leaner, avoids potential dependency conflicts, and easier to test.

3: Form & Data Handling

PlayFast comes with Slick and proposes a convention of how you manage data. It offers examples how you can handle form data; show validation errors; passing data between the database, the backend, and the frontend; and avoid the N+1 query problem.

Parting Thought

I'd say the learning curve of Scala and PlayFramework is steep. There are a lot of components to get right in order to make it production-ready and development-ready.

As I mentioned at the beginning, it would have taken days to weeks, with a large amount of googling, to set up these components and code conventions. The compiler flags below are examples where I spent hours googling and trying out different answers before getting to a working solution ... by reading the Scala 3 compiler code directly:

// Silence warnings on the generated code (e.g. from Play) because we don't have control over it.
// Also, silence the warnings on the test code.
"-Wconf:msg=.*unused value of type.*&src=(target|test)/.*:silent",
"-Wconf:msg=.*unused import.*&src=target/.*:silent",

Another example is how to reset the database before each test as shown below:

 def resetDatabase(): Unit = {
    import framework.PostgresProfile.api.*
    // Initialize play.api.db.evolutions.DefaultEvolutionsApi, so we can set the log level dynamically.
    app.injector.instanceOf[EvolutionsApi]

    // Silence the logs from evolutions
    LoggerFactory
      .getLogger("play.api.db.evolutions")
      .asInstanceOf[ch.qos.logback.classic.Logger]
      .setLevel(Level.INFO)

    val db = dbConfig.db

    val tables = await(db.run {
      sql"SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename ASC;"
        .as[String]
    })

    tables.foreach { table =>
      await(db.run {
        sqlu"""DROP TABLE IF EXISTS "#$table" CASCADE;"""
      })
    }

    app.injector.instanceOf[EvolutionsApi].applyFor("default")
  }

You probably don't want to spend time figuring out how to do these menial things, but they are absolutely important to having a good development experience and a production-ready system.

Once you've got those components and conventions in place, it's the most productive web framework for me so far. PlayFramework is generally type-safe. Scala is succinct and statically typed. Refactoring is a breeze. If you ever refactor a Ruby on Rails project, you'd know what I mean 😈. And we can use Java libraries!

I hope PlayFast is useful to you all as a basis for your new application and/or, at least, as an educational tool.

If you have any question or feature request, please open an issue in the PlayFast Github repository: https://github.com/tanin47/playfast

Thank you!

Subscribe to tanin

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe