Scala Web Project Examples

This page contains examples of using Mill as a build tool for web-applications. It covers setting up a basic backend server, Todo-MVC app, topics like cache busting, as well as usage of Scala.js both as standalone modules as well as integrated with your backend Scala-JVM web server.

TodoMVC Web App

build.mill (download, browse)
package build
import mill._, scalalib._

object `package` extends RootModule with ScalaModule {
  def scalaVersion = "2.13.8"
  def ivyDeps = Agg(
    ivy"com.lihaoyi::cask:0.9.1",
    ivy"com.lihaoyi::scalatags:0.12.0"
  )

  object test extends ScalaTests {
    def testFramework = "utest.runner.Framework"

    def ivyDeps = Agg(
      ivy"com.lihaoyi::utest::0.8.4",
      ivy"com.lihaoyi::requests::0.6.9"
    )
  }
}

This example demonstrates how to set up a simple Scala webserver implementing the popular Todo-MVC demo application. It includes a test suite that spins up the web server locally and makes HTTP requests against it.

> ./mill test
+ webapp.WebAppTests.simpleRequest...

> ./mill runBackground

> curl http://localhost:8080
...What needs to be done...
...

> ./mill clean runBackground

Webapp Cache Busting

build.mill (download, browse)
package build
import mill._, scalalib._
import java.util.Arrays

object `package` extends RootModule with ScalaModule {
  def scalaVersion = "2.13.8"
  def ivyDeps = Agg(
    ivy"com.lihaoyi::cask:0.9.1",
    ivy"com.lihaoyi::scalatags:0.12.0",
    ivy"com.lihaoyi::os-lib:0.10.7"
  )

  def resources = Task {
    val hashMapping = for {
      resourceRoot <- super.resources()
      path <- os.walk(resourceRoot.path)
      if os.isFile(path)
    } yield hashFile(path, resourceRoot.path, Task.dest)

    os.write(
      Task.dest / "hashed-resource-mapping.json",
      upickle.default.write(hashMapping.toMap, indent = 4)
    )

    Seq(PathRef(Task.dest))
  }

  object test extends ScalaTests {
    def testFramework = "utest.runner.Framework"
    def ivyDeps = Agg(
      ivy"com.lihaoyi::utest::0.8.4",
      ivy"com.lihaoyi::requests::0.6.9"
    )
  }

  def hashFile(path: os.Path, src: os.Path, dest: os.Path) = {
    val hash = Integer.toHexString(Arrays.hashCode(os.read.bytes(path)))
    val relPath = path.relativeTo(src)
    val ext = if (relPath.ext == "") "" else s".${relPath.ext}"
    val hashedPath = relPath / os.up / s"${relPath.baseName}-$hash$ext"
    os.copy(path, dest / hashedPath, createFolders = true)
    (relPath.toString(), hashedPath.toString())
  }
}

This example demonstrates how to implement webapp "cache busting" in Mill, where we serve static files with a hash appended to the filename, and save a mapping of filename to hashed filename so that the web server can serve HTML that references the appropriately hashed file paths. This allows us to deploy the static files behind caches with long expiration times, while still having the web app immediately load updated static files after a deploy (since the HTML will reference new hashed paths that are not yet in the cache).

We do this in an overrride of the resources task, that loads super.resources(), hashes the files within it using Arrays.hashCode, and copies the files to a new hashed path saving the overall mapping to a hashed-resource-mapping.json. The webapp then loads the mapping at runtime and uses it to serve HTML referencing the hashed paths, but without paying the cost of hashing the static resource files at runtime.

> ./mill test
+ webapp.WebAppTests.simpleRequest ...

> ./mill runBackground

> curl http://localhost:8081
...What needs to be done...
...

> curl http://localhost:8081/static/main-6da98e99.js # mac/linux
initListeners()

> ./mill clean runBackground

Scala.js Modules

build.mill (download, browse)
package build
import mill._, scalalib._, scalajslib._

object foo extends ScalaJSModule {
  def scalaVersion = "2.13.14"
  def scalaJSVersion = "1.16.0"
  def ivyDeps = Agg(ivy"com.lihaoyi::scalatags::0.12.0")
  object test extends ScalaJSTests {
    def ivyDeps = Agg(ivy"com.lihaoyi::utest::0.8.4")
    def testFramework = "utest.runner.Framework"
  }
}

This build defines a single ScalaJSModule with a test suite. ScalaJSModule is similar to ScalaModule, except it requires a scalaJSVersion to be provided.

By default, Scala.js code gets access to the scala.scalajs.js package, which contains the core JS APIs like js.JSON, js.Date, etc. ivyDeps of Scala.js-compatible dependencies can be given, which need two colons (::) on the right to indicate it’s a Scala.js dependency. These can be both Scala libraries compiled to JS, or facades for Javascript libraries. If running in the browser, you can use the https://github.com/scala-js/scala-js-dom facade to access the browser DOM APIs.

Normal tasks like compile, run, or test work as expected, with run and test calling node to run in a Javascript environment rather than in the JVM. There is also additional fastLinkJS and fullLinkJS commands that compile the module into a single Javascript file, which you can then distribute or deploy with your web application

> ./mill foo.run
<h1>Hello World</h1>
stringifiedJsObject: ["hello","world","!"]

> ./mill foo.test
+ foo.FooTests.hello...

> ./mill show foo.fullLinkJS # mac/linux
{
...
..."jsFileName": "main.js",
  "dest": ".../out/foo/fullLinkJS.dest"
}

> node out/foo/fullLinkJS.dest/main.js # mac/linux
<h1>Hello World</h1>
stringifiedJsObject: ["hello","world","!"]

Note that running Scala.js modules locally requires the node Javascript runtime to be installed on your machine.

Scala.js Webserver Integration

build.mill (download, browse)
package build
import mill._, scalalib._, scalajslib._

object `package` extends RootModule with ScalaModule {

  def scalaVersion = "2.13.14"
  def ivyDeps = Agg(
    ivy"com.lihaoyi::cask:0.9.1",
    ivy"com.lihaoyi::scalatags:0.12.0"
  )

  def resources = Task {
    os.makeDir(Task.dest / "webapp")
    val jsPath = client.fastLinkJS().dest.path
    // Move main.js[.map]into the proper filesystem position
    // in the resource folder for the web server code to pick up
    os.copy(jsPath / "main.js", Task.dest / "webapp/main.js")
    os.copy(jsPath / "main.js.map", Task.dest / "webapp/main.js.map")
    super.resources() ++ Seq(PathRef(Task.dest))
  }

  object test extends ScalaTests {
    def testFramework = "utest.runner.Framework"

    def ivyDeps = Agg(
      ivy"com.lihaoyi::utest::0.8.4",
      ivy"com.lihaoyi::requests::0.6.9"
    )
  }

  object client extends ScalaJSModule {
    def scalaVersion = "2.13.14"
    def scalaJSVersion = "1.16.0"
    def ivyDeps = Agg(ivy"org.scala-js::scalajs-dom::2.2.0")
  }
}

A minimal example of a Scala-JVM backend server wired up with a Scala.js front-end. The backend code is identical to the TodoMVC Web App example, but we replace the main.js client side code with the Javascript output of ClientApp.scala.

Note that the client-side Scala code is the simplest 1-to-1 translation of the original Javascript, using scalajs-dom, as this example is intended to demonstrate the build.mill config in Mill. A real codebase is likely to use Javascript or Scala UI frameworks to manage the UI, but those are beyond the scope of this example.

> ./mill test
+ webapp.WebAppTests.simpleRequest ...

> ./mill runBackground

> curl http://localhost:8082
...What needs to be done...
...

> curl http://localhost:8082/static/main.js
..."org.scalajs.linker.runtime.RuntimeLong"...
...

> ./mill clean runBackground

Scala.js/Scala-JVM Code Sharing

build.mill (download, browse)
package build
import mill._, scalalib._, scalajslib._

trait AppScalaModule extends ScalaModule {
  def scalaVersion = "3.3.3"
}

trait AppScalaJSModule extends AppScalaModule with ScalaJSModule {
  def scalaJSVersion = "1.16.0"
}

object `package` extends RootModule with AppScalaModule {
  def moduleDeps = Seq(shared.jvm)
  def ivyDeps = Agg(ivy"com.lihaoyi::cask:0.9.1")

  def resources = Task {
    os.makeDir(Task.dest / "webapp")
    val jsPath = client.fastLinkJS().dest.path
    os.copy(jsPath / "main.js", Task.dest / "webapp/main.js")
    os.copy(jsPath / "main.js.map", Task.dest / "webapp/main.js.map")
    super.resources() ++ Seq(PathRef(Task.dest))
  }

  object test extends ScalaTests with TestModule.Utest {

    def ivyDeps = Agg(
      ivy"com.lihaoyi::utest::0.8.4",
      ivy"com.lihaoyi::requests::0.6.9"
    )
  }

  object shared extends Module {
    trait SharedModule extends AppScalaModule with PlatformScalaModule {
      def ivyDeps = Agg(
        ivy"com.lihaoyi::scalatags::0.12.0",
        ivy"com.lihaoyi::upickle::3.0.0"
      )
    }

    object jvm extends SharedModule
    object js extends SharedModule with AppScalaJSModule
  }

  object client extends AppScalaJSModule {
    def moduleDeps = Seq(shared.js)
    def ivyDeps = Agg(ivy"org.scala-js::scalajs-dom::2.2.0")
  }
}

A Scala-JVM backend server wired up with a Scala.js front-end, with a shared module containing code that is used in both client and server. Rather than the server sending HTML for the initial page load and HTML for page updates, it sends HTML for the initial load and JSON for page updates which is then rendered into HTML on the client.

The JSON serialization logic and HTML generation logic in the shared module is shared between client and server, and uses libraries like uPickle and Scalatags which work on both ScalaJVM and Scala.js. This allows us to freely move code between the client and server, without worrying about what platform or language the code was originally implemented in.

This is a minimal example of shared code compiled to ScalaJVM and Scala.js, running on both client and server, meant for illustrating the build configuration. A full exploration of client-server code sharing techniques is beyond the scope of this example.

> ./mill test
+ webapp.WebAppTests.simpleRequest ...

> ./mill runBackground

> curl http://localhost:8083
...What needs to be done...
...

> curl http://localhost:8083/static/main.js
...Scala.js...
...

> ./mill clean runBackground

Publishing Cross-Platform Scala Modules

build.mill (download, browse)
package build
import mill._, scalalib._, scalajslib._, publish._

object foo extends Cross[FooModule]("2.13.14", "3.3.3")
trait FooModule extends Cross.Module[String] {
  trait Shared extends CrossScalaModule with CrossValue with PlatformScalaModule
      with PublishModule {
    def publishVersion = "0.0.1"

    def pomSettings = PomSettings(
      description = "Hello",
      organization = "com.lihaoyi",
      url = "https://github.com/lihaoyi/example",
      licenses = Seq(License.MIT),
      versionControl = VersionControl.github("lihaoyi", "example"),
      developers = Seq(Developer("lihaoyi", "Li Haoyi", "https://github.com/lihaoyi"))
    )

    def ivyDeps = Agg(ivy"com.lihaoyi::scalatags::0.12.0")
  }

  trait FooTestModule extends TestModule {
    def ivyDeps = Agg(ivy"com.lihaoyi::utest::0.8.4")
    def testFramework = "utest.runner.Framework"
  }

  trait SharedJS extends Shared with ScalaJSModule {
    def scalaJSVersion = "1.16.0"
  }

  object bar extends Module {
    object jvm extends Shared {
      object test extends ScalaTests with FooTestModule
    }
    object js extends SharedJS {
      object test extends ScalaJSTests with FooTestModule
    }
  }

  object qux extends Module {
    object jvm extends Shared {
      def moduleDeps = Seq(bar.jvm)
      def ivyDeps = super.ivyDeps() ++ Agg(ivy"com.lihaoyi::upickle::3.0.0")

      object test extends ScalaTests with FooTestModule
    }

    object js extends SharedJS {
      def moduleDeps = Seq(bar.js)

      object test extends ScalaJSTests with FooTestModule
    }
  }
}

This example demonstrates how to publish Scala modules which are both cross-version and cross-platform: running on both Scala 2.13.14/3.3.3 as well as Scala-JVM/JS.

> ./mill show foo[2.13.14].bar.jvm.sources
[
  ".../foo/bar/src",
  ".../foo/bar/src-jvm",
  ".../foo/bar/src-2.13.14",
  ".../foo/bar/src-2.13.14-jvm",
  ".../foo/bar/src-2.13",
  ".../foo/bar/src-2.13-jvm",
  ".../foo/bar/src-2",
  ".../foo/bar/src-2-jvm"
]

> ./mill show foo[3.3.3].qux.js.sources
[
  ".../foo/qux/src",
  ".../foo/qux/src-js",
  ".../foo/qux/src-3.3.3",
  ".../foo/qux/src-3.3.3-js",
  ".../foo/qux/src-3.3",
  ".../foo/qux/src-3.3-js",
  ".../foo/qux/src-3",
  ".../foo/qux/src-3-js"
]

> ./mill foo[2.13.14].qux.jvm.run
Bar.value: <p>world Specific code for Scala 2.x</p>
Parsing JSON with ujson.read
Qux.main: Set(<p>i</p>, <p>cow</p>, <p>me</p>)

> ./mill foo[3.3.3].qux.js.run
Bar.value: <p>world Specific code for Scala 3.x</p>
Parsing JSON with js.JSON.parse
Qux.main: Set(<p>i</p>, <p>cow</p>, <p>me</p>)

> ./mill foo[3.3.3].__.js.test
+ bar.BarTests.test ...  <p>world Specific code for Scala 3.x</p>
+ qux.QuxTests.parseJsonGetKeys ...  Set(i, cow, me)

> ./mill __.publishLocal
Publishing Artifact(com.lihaoyi,foo-bar_sjs1_2.13,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,foo-bar_2.13,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,foo-qux_sjs1_2.13,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,foo-qux_2.13,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,foo-bar_sjs1_3,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,foo-bar_3,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,foo-qux_sjs1_3,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,foo-qux_3,0.0.1) to ivy repo...

Publishing Cross-Platform Scala Modules Alternative

build.mill (download, browse)
package build
import mill._, scalalib._, scalajslib._, publish._

trait Shared extends CrossScalaModule with PlatformScalaModule with PublishModule {
  def publishVersion = "0.0.1"

  def pomSettings = PomSettings(
    description = "Hello",
    organization = "com.lihaoyi",
    url = "https://github.com/lihaoyi/example",
    licenses = Seq(License.MIT),
    versionControl = VersionControl.github("lihaoyi", "example"),
    developers = Seq(Developer("lihaoyi", "Li Haoyi", "https://github.com/lihaoyi"))
  )

  def ivyDeps = Agg(ivy"com.lihaoyi::scalatags::0.12.0")
}

trait SharedTestModule extends TestModule {
  def ivyDeps = Agg(ivy"com.lihaoyi::utest::0.8.4")
  def testFramework = "utest.runner.Framework"
}

trait SharedJS extends Shared with ScalaJSModule {
  def scalaJSVersion = "1.16.0"
}

val scalaVersions = Seq("2.13.14", "3.3.3")

object bar extends Module {
  object jvm extends Cross[JvmModule](scalaVersions)
  trait JvmModule extends Shared {
    object test extends ScalaTests with SharedTestModule
  }

  object js extends Cross[JsModule](scalaVersions)
  trait JsModule extends SharedJS {
    object test extends ScalaJSTests with SharedTestModule
  }
}

object qux extends Module {
  object jvm extends Cross[JvmModule](scalaVersions)
  trait JvmModule extends Shared {
    def moduleDeps = Seq(bar.jvm())
    def ivyDeps = super.ivyDeps() ++ Agg(ivy"com.lihaoyi::upickle::3.0.0")

    object test extends ScalaTests with SharedTestModule
  }

  object js extends Cross[JsModule](scalaVersions)
  trait JsModule extends SharedJS {
    def moduleDeps = Seq(bar.js())

    object test extends ScalaJSTests with SharedTestModule
  }
}

This example demonstrates an alternative way of defining your cross-platform cross-version modules: rather than wrapping them all in a foo cross-module to provide the different versions, we instead give each module bar.jvm, bar.js, qux.jvm, qux.js its own Cross module. This approach can be useful if the different cross modules need to support different sets of Scala versions, as it allows you to specify the scalaVersions passed to each individual cross module separately.

> ./mill show qux.js[3.3.3].sources
[
  ".../qux/src",
  ".../qux/src-js",
  ".../qux/src-3.3.3",
  ".../qux/src-3.3.3-js",
  ".../qux/src-3.3",
  ".../qux/src-3.3-js",
  ".../qux/src-3",
  ".../qux/src-3-js"
]

> ./mill show qux.js[3.3.3].test.sources
[
  ".../qux/test/src",
  ".../qux/test/src-js",
  ".../qux/test/src-3.3.3",
  ".../qux/test/src-3.3.3-js",
  ".../qux/test/src-3.3",
  ".../qux/test/src-3.3-js",
  ".../qux/test/src-3",
  ".../qux/test/src-3-js"
]

> ./mill qux.jvm[2.13.14].run
Bar.value: <p>world Specific code for Scala 2.x</p>
Parsing JSON with ujson.read
Qux.main: Set(<p>i</p>, <p>cow</p>, <p>me</p>)

> ./mill __.js[3.3.3].test
+ bar.BarTests.test ...  <p>world Specific code for Scala 3.x</p>
+ qux.QuxTests.parseJsonGetKeys ...  Set(i, cow, me)

> ./mill __.publishLocal
...
Publishing Artifact(com.lihaoyi,bar_sjs1_2.13,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,bar_2.13,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,qux_sjs1_2.13,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,qux_2.13,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,bar_sjs1_3,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,bar_3,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,qux_sjs1_3,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,qux_3,0.0.1) to ivy repo...

Scala.js WebAssembly Example

build.mill (download, browse)
package build
import mill._, scalalib._, scalajslib._
import mill.scalajslib.api._

object wasm extends ScalaJSModule {
  override def scalaVersion = "2.13.14"

  override def scalaJSVersion = "1.17.0"

  override def moduleKind = ModuleKind.ESModule

  override def moduleSplitStyle = ModuleSplitStyle.FewestModules

  override def scalaJSExperimentalUseWebAssembly = true
}

This build defines a single ScalaJSModule that uses the WASM backend of the scala JS linker. The release notes that introduced scalaJS wasm are here; https://www.scala-js.org/news/2024/09/28/announcing-scalajs-1.17.0/ and are worth reading. They include information such as the scala JS requirements to successfully emit wasm, the flags needed to run in browser and the minimum node version (22) required to actually run the wasm output.

> ./mill show wasm.fastLinkJS # mac/linux
{
...
..."jsFileName": "main.js",
...
  "dest": ".../out/wasm/fastLinkJS.dest"
}

> node --experimental-wasm-exnref out/wasm/fastLinkJS.dest/main.js # mac/linux
hello  wasm!

Here we see that scala JS emits a single WASM module, as well as a loader and main.js file. main.js is the entry point of the program, and calls into the wasm module.