The Scala Toolkit

How to serve a dynamic page?

Language

You can declare a dependency on Cask with the following using directive:

//> using dep "com.lihaoyi::cask::0.9.2"

In your build.sbt, you can add a dependency on Cask:

lazy val example = project.in(file("example"))
  .settings(
    scalaVersion := "3.4.2",
    libraryDependencies += "com.lihaoyi" %% "cask" % "0.9.2",
    fork := true
  )

In your build.sc, you can add a dependency on Cask:

object example extends RootModule with ScalaModule {
  def scalaVersion = "3.3.3"
  def ivyDeps = Agg(
    ivy"com.lihaoyi::cask::0.9.2"
  )
}

Serving dynamically generated content

You can create an endpoint returning dynamically generated content with the @cask.get annotation.

import java.time.ZonedDateTime

object Example extends cask.MainRoutes {
  @cask.get("/time")
  def dynamic(): String = s"Current date is: ${ZonedDateTime.now()}"

  initialize()
}
import java.time.ZonedDateTime

object Example extends cask.MainRoutes:
  @cask.get("/time")
  def dynamic(): String = s"Current date is: ${ZonedDateTime.now()}"

  initialize()

The example above creates an endpoint that returns the current date and time available at /time. The exact response will be recreated every time you refresh the webpage.

Since the endpoint method has the String output type, the result will be sent with the text/plain content type. If you want an HTML output to be interpreted by the browser, you will need to set the Content-Type header manually or use the Scalatags templating library, supported by Cask.

Running the example

Run the example the same way as before, assuming you use the same project structure as described in the static file tutorial.

In the terminal, the following command will start the server:

scala-cli run Example.scala

In the terminal, the following command will start the server:

sbt example/run

In the terminal, the following command will start the server:

./mill run

Access the endpoint at http://localhost:8080/time. You should see a result similar to the one below.

Current date is: 2024-07-22T09:07:05.752534+02:00[Europe/Warsaw]

Using path segments

Cask gives you the ability to access segments of the URL path within the endpoint function. Building on the example above, you can add a segment to specify that the endpoint should return the date and time in a given city.

import java.time.{ZoneId, ZonedDateTime}

object Example extends cask.MainRoutes {

  private def getZoneIdForCity(city: String): Option[ZoneId] = {
    import scala.jdk.CollectionConverters._
    ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of)
  }

  @cask.get("/time/:city")
  def dynamicWithCity(city: String): String = {
    getZoneIdForCity(city) match {
      case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}"
      case None => s"Couldn't find time zone for city $city"
    }
  }

  initialize()
}
import java.time.{ZoneId, ZonedDateTime}

object Example extends cask.MainRoutes:

  private def getZoneIdForCity(city: String): Option[ZoneId] =
    import scala.jdk.CollectionConverters.*
    ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of)
  
  @cask.get("/time/:city")
  def dynamicWithCity(city: String): String =
    getZoneIdForCity(city) match
      case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}"
      case None => s"Couldn't find time zone for city $city"

  initialize()

In the example above, the :city segment in /time/:city is available through the city argument of the endpoint method. The name of the argument must be identical to the segment name. The getZoneIdForCity helper method finds the timezone for a given city, and then the current date and time are translated to that timezone.

Accessing the endpoint at http://localhost:8080/time/Paris will result in:

Current date is: 2024-07-22T09:08:33.806259+02:00[Europe/Paris]

You can use more than one path segment in an endpoint by adding more arguments to the endpoint method. It’s also possible to use paths with an unspecified number of segments (for example /path/foo/bar/baz/) by giving the endpoint method an argument with cask.RemainingPathSegments type. Consult the documentation for more details.

Using HTML templates

To create an HTML response, you can combine Cask with the Scalatags templating library.

Import the Scalatags library:

Add the Scalatags dependency in Example.sc file:

//> using dep "com.lihaoyi::scalatags::0.13.1"

Add the Scalatags dependency in build.sbt file:

libraryDependencies += "com.lihaoyi" %% "scalatags" % "0.13.1"

Add the Scalatags dependency in build.cs file:

ivy"com.lihaoyi::scalatags::0.13.1"

Now the example above can be rewritten to use a template. Cask will build a response out of the doctype automatically, setting the Content-Type header to text/html.

import java.time.{ZoneId, ZonedDateTime}
import scalatags.Text.all._

object Example extends cask.MainRoutes {

  private def getZoneIdForCity(city: String): Option[ZoneId] = {
    import scala.jdk.CollectionConverters._
    ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of)
  }
  
  @cask.get("/time/:city")
  def dynamicWithCity(city: String): doctype = {
    val text = getZoneIdForCity(city) match {
      case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}"
      case None => s"Couldn't find time zone for city $city"
    }

    doctype("html")(
      html(
        body(
          p(text)
        )
      )
    )
  }

  initialize()
}
import java.time.{ZoneId, ZonedDateTime}
import scalatags.Text.all.*

object Example extends cask.MainRoutes:

  private def getZoneIdForCity(city: String): Option[ZoneId] =
    import scala.jdk.CollectionConverters.*
    ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of)

  @cask.get("/time/:city")
  def dynamicWithCity(city: String): doctype =
    val text = getZoneIdForCity(city) match
      case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}"
      case None => s"Couldn't find time zone for city $city"
    doctype("html")(
      html(
        body(
          p(text)
        )
      )
    )

  initialize()

Here we get the text of the response and wrap it in a Scalatags template. Notice that the return type changed from String to doctype.

Contributors to this page: