Tutorial: Basic Full Stack App With Scala 3

post-thumb

The goal of this tutorial is to setup a fullstack app in Scala 3. The idea of this tutorial is to learn and understand the different pieces needed for a fullstack app in Scala. Thus, we are going through each step and explaining our every choice. We also link to relevant documentation.

There are several components that you will need to have your fullstack app:

  • a server that provides web services that are the backend of our app
  • a client that works in the browser and provides a UI for the user and communicates with the backend

The architecture is simple, but we also need some supporting infrastructure:

  • Our build tool needs to be able to build both projects as subprojects of the same project.
  • We would like to have autoreload of the client when the code is updated
  • We would like to have autoreload of the server when the code is updated

I’m assuming that you are using Linux, Mac OS X or Windows with WSL for this tutorial. Commands have been tested on a Mac using bash shell.

Setting Up the Server

Create the Project and Server Subproject

We start from scratch with a template for a Scala 3 project:

$ sbt new scala/scala3.g8
[info] resolving Giter8 0.16.2...
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
A template to demonstrate a minimal Scala 3 application

name [Scala 3 Project Template]: fullstack-scala3

Template applied in /Users/mundacho/temp/fullstack-laminar/./fullstack-scala3

This creates a Hello, World! project in Scala 3. Now, let’s customize it. First thing, we need a subproject at folder server. We create it:

$ mkdir server

The next step is to move our src folder to that new folder:

$ mv src server/

We now update our build.sbt file. We do the following:

  • Step 1: We create the new server subproject
  • Step 2: We aggregate the server project
// build.sbt
val scala3Version = "3.3.1"

lazy val root = project
  .in(file("."))
  .settings(
    name := "fullstack-scala3",
    version := "0.1.0-SNAPSHOT",
    scalaVersion := scala3Version
  )
  .aggregate(server) // Step 2: We aggregate the server project

lazy val server = project // Step 1: We create this block
  .in(file("./server"))
  .settings(
    name := "server",
    Compile / run / fork := true,
    libraryDependencies ++= Seq(
      "org.scalameta" %% "munit" % "0.7.29" % Test
    )
  )

Until now, nothing new under the sun. We are just creating a simple Scala 3 project that happens to have a subproject.

Setup the Webserver and Webservice

For this tutorial, we are using:

If you are familiar with other tools please feel free to use them. circe has the advantage of being usable for both for the server and the client, and offers an integration with http4s. However, there are other choices such as uPickle and cask from the Li Haoyi ecosystem, or ZIO http and ZIO Json from ZIO. Whatever suits you better, as long as you can create a web service on it.

For us, we can just go and checkout the documentation of http4s and modify our server project accordingly.

// the root part above stays the same
// build.sbt
val http4sVersion = "0.23.24"

val circeVersion = "0.14.6"

lazy val server = project
  .in(file("./server"))
  .settings(
    name := "server",
    Compile / run / fork := true,
    scalaVersion := scala3Version,
    libraryDependencies ++= Seq(
      "org.http4s" %% "http4s-ember-client" % http4sVersion,
      "org.http4s" %% "http4s-ember-server" % http4sVersion,
      "org.http4s" %% "http4s-dsl" % http4sVersion,
      "org.slf4j" % "slf4j-simple" % "2.0.9",
      "org.http4s" %% "http4s-circe" % http4sVersion,
      "io.circe" %% "circe-generic" % circeVersion,
      "org.scalameta" %% "munit" % "0.7.29" % Test
    )
  )

Now that we have web server, we needs some service. We will serve a list of products. Let us create a model and a data access object to feed our web service in the ProductDAO.scala file:

// ProductDAO.scala
package com.idiomaticsoft.app

case class Product(code: String, description: String, price: Double)

object ProductDAO:

  def findProducts(): List[Product] =
    List(
      Product("CODE1", "Product 1", 10),
      Product("CODE2", "Product 2", 130),
      Product("CODE3", "Product 3", 200),
      Product("CODE4", "Product 4", 200),
      Product("CODE5", "Product 5", 200)
    )

Finally, we update our main class:

// Main.scala
package com.idiomaticsoft.app

import cats.effect.IO
import cats.effect.IOApp
import cats.effect.*
import com.comcast.ip4s.*
import io.circe.generic.auto.*
import io.circe.syntax.*
import org.http4s.*
import org.http4s.circe.*
import org.http4s.dsl.io.*
import org.http4s.ember.server.EmberServerBuilder
import org.http4s.implicits.*
import org.http4s.server.Router

object Main extends IOApp.Simple:

	// 1: Create the product service
  val productService = HttpRoutes.of[IO] {
    case GET -> Root / "products" =>
      Ok(ProductDAO.findProducts().asJson)
  }
	// 2: Allocate a route to the service in the router
  val httpApp = Router("/api" -> productService).orNotFound

	// 3: Build the actual server
  val server = EmberServerBuilder
    .default[IO]
    .withHost(ipv4"0.0.0.0")
    .withPort(port"8080")
    .withHttpApp(httpApp)
    .build

	// 4: Launch the server in the application loop
  val run = for {
    _ <- server.allocated
    _ <- IO.never // this is needed so that the server keeps running
  } yield ()

The new main class defines the following:

  1. A product service that serves th list of products from our ProductDAO.
  2. A router that maps the service under the /api route.
  3. The actual server, listening on all IPv4 interfaces on port 8080 and that uses our router
  4. A main method that launchs our server. The main method makes sure that it stays running indefinitely after launching the server.

We can now run our server using sbt with the following command:

$ sbt server/run
[info] welcome to sbt 1.9.7 (AdoptOpenJDK Java 11.0.9.1)
[info] loading settings for project global-plugins from plugins.sbt ...
[info] loading global plugins from /Users/mundacho/.sbt/1.0/plugins
[info] loading settings for project fullstack-scala3-build-build from metals.sbt ...
[info] loading project definition from /Users/mundacho/Desktop/fullstack-laminar/fullstack-scala3/project/project
[info] loading settings for project fullstack-scala3-build from metals.sbt ...
[info] loading project definition from /Users/mundacho/Desktop/fullstack-laminar/fullstack-scala3/project
[success] Generated .bloop/fullstack-scala3-build.json
[success] Total time: 2 s, completed Nov 23, 2023, 9:43:46 PM
[info] loading settings for project root from build.sbt ...
[info] set current project to fullstack-scala3 (in build file:/Users/mundacho/Desktop/fullstack-laminar/fullstack-scala3/)
[info] running (fork) com.idiomaticsoft.app.Main 
[error] [io-compute-14] INFO org.http4s.ember.server.EmberServerBuilderCompanionPlatform - Ember-Server service bound to address: [::]:8080

You can test that the JSON encoder and service works by opening a terminal and running:

$ curl localhost:8080/api/products
[{"code":"CODE1","description":"Product 1","price":10.0},{"code":"CODE2","description":"Product 2","price":130.0},{"code":"CODE3","description":"Product 3","price":200.0},{"code":"CODE4","description":"Product 4","price":200.0},{"code":"CODE5","description":"Product 5","price":200.0}]

Setting Up the Client

Scala JS has excellent documentation, we will be following the tutorial for Scala JS and Vite. Vite.js is a tool that allows to serve JS applications like a webserver, but can do some cool stuff like hot reloading modules and in particular your Javascript application. Yes, you are writing Scala, but you application is compile to standard JS in the end, and there, Vite.js is your friend.

Creating the Vite.js Project

You need to have installed node and npm to make this work. Once ther, you can create the client. From the root directory of your main project do:

$ npm create vite@4.1.0
✔ Project name: … client
✔ Select a framework: › Vanilla
✔ Select a variant: › JavaScript

Scaffolding project in fullstack-scala3/client...

Done. Now run:

  cd client
  npm install
  npm run dev

Now, follow up as requested and do:

$ cd client/
$ npm install

added 9 packages, and audited 10 packages in 5s

3 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

We will modify a little bit the template before doing the npm run dev, which will actually launch the Vite.js server.

Setting up our Template

We want our app to look cool, so we will download a template to make it better. We can get a free template from StartBootstrap.com. The bootstrap template contains the webpages, some JS code and CSS style sheets for your app. We remove some menus and replace the index.html file from the vite.js template with this one from the bootstrap template:

<!DOCTYPE html>
<html lang="en">

<head>
   <meta charset="utf-8" />
   <meta name="viewport"
         content="width=device-width, initial-scale=1, shrink-to-fit=no" />
   <meta name="description"
         content="" />
   <meta name="author"
         content="" />
   <title>Simple Sidebar - Start Bootstrap Template</title>
   <!-- Favicon-->
   <link rel="icon"
         type="image/x-icon"
         href="assets/favicon.ico" />
</head>

<body>
   <div class="d-flex"
        id="wrapper">
      <!-- Sidebar-->
      <div class="border-end bg-white"
           id="sidebar-wrapper">
         <div class="sidebar-heading border-bottom bg-light">Start Bootstrap</div>
         <div class="list-group list-group-flush">
            <a class="list-group-item list-group-item-action list-group-item-light p-3"
               href="#!">Demo</a>
         </div>
      </div>
      <!-- Page content wrapper-->
      <div id="page-content-wrapper">
         <!-- Top navigation-->
         <nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
            <div class="container-fluid">
               <button class="btn btn-primary"
                       id="sidebarToggle">Toggle Menu</button>
               <button class="navbar-toggler"
                       type="button"
                       data-bs-toggle="collapse"
                       data-bs-target="#navbarSupportedContent"
                       aria-controls="navbarSupportedContent"
                       aria-expanded="false"
                       aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button>
               <div class="collapse navbar-collapse"
                    id="navbarSupportedContent">
                  <ul class="navbar-nav ms-auto mt-2 mt-lg-0">
                     <li class="nav-item active"><a class="nav-link"
                           href="#!">Home</a></li>
                     <li class="nav-item"><a class="nav-link"
                           href="#!">Link</a></li>
                     <li class="nav-item dropdown">
                        <a class="nav-link dropdown-toggle"
                           id="navbarDropdown"
                           href="#"
                           role="button"
                           data-bs-toggle="dropdown"
                           aria-haspopup="true"
                           aria-expanded="false">Dropdown</a>
                        <div class="dropdown-menu dropdown-menu-end"
                             aria-labelledby="navbarDropdown">
                           <a class="dropdown-item"
                              href="#!">Action</a>
                           <a class="dropdown-item"
                              href="#!">Another action</a>
                           <div class="dropdown-divider"></div>
                           <a class="dropdown-item"
                              href="#!">Something else here</a>
                        </div>
                     </li>
                  </ul>
               </div>
            </div>
         </nav>
         <!-- Page content-->
         <div class="container-fluid">
            <h1 class="mt-4">App</h1>
            <div id="app"></div>
         </div>
      </div>
   </div>
   <!-- Bootstrap core JS-->
   <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
   <!-- Core theme JS-->
   <script type="module"
           src="/main.js"></script>
</body>

</html>

The bootstrap template also includes a CSS (style.css) file, we just copy and paste it to the root path of our client folder, you can indeed just replace the one that the vite.js template created for you. The file is too large to include here, but it is included in the template files under the css folder. Under the js folder in the bootstrap template there is a script.js file. We copy and paste it into the root path of the client folder.

Finally, you need to replace the contents of the main.js file included in the Vite.js template with the following:

import './style.css'
import './scripts.js'
import 'scalajs:main.js'

Now we have our UI ready but Vite.js does not know about Scala JS, and our own build.sbt does not know about our Scala JS project.

Setting Up Sbt

Let’s start with sbt. The first thing is to setup the ScalaJS plugin. This is done by creating the file plugins.sbt in the project folder of our root project (not the client nor the server, the actual root):

addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.14.0")

Then, we add the configuration to the build.sbt file:

import org.scalajs.linker.interface.ModuleSplitStyle // 1. You need to import this

lazy val root = project
  .in(file("."))
  .settings(
    name := "fullstack-scala3",
    version := "0.1.0-SNAPSHOT",
    scalaVersion := scala3Version
  )
  .aggregate(server, client) // 2. You need aggregate the client

// [..] server stays the same

lazy val client = project
  .in(file("./client"))
  .enablePlugins(ScalaJSPlugin)
  .settings(
    scalaVersion := scala3Version,
    Compile / run / fork := true,
    // Tell Scala.js that this is an application with a main method
    scalaJSUseMainModuleInitializer := true,

    /* Configure Scala.js to emit modules in the optimal way to
     * connect to Vite's incremental reload.
     * - emit ECMAScript modules
     * - emit as many small modules as possible for classes in the "client" package
     * - emit as few (large) modules as possible for all other classes
     *   (in particular, for the standard library)
     */
    scalaJSLinkerConfig ~= {
      _.withModuleKind(ModuleKind.ESModule)
        .withModuleSplitStyle(
          ModuleSplitStyle.SmallModulesFor(List("client"))
        )
    },

    /* Depend on the scalajs-dom library.
     * It provides static types for the browser DOM APIs.
     */
    libraryDependencies ++= Seq(
      "org.scala-js" %%% "scalajs-dom" % "2.4.0",
      "io.circe" %%% "circe-core" % circeVersion,
      "io.circe" %%% "circe-parser" % circeVersion,
      "io.circe" %%% "circe-generic" % circeVersion
    )
  )

The configuration adds support for the scalajs-dom library and the circe library, the same we used for the server. The rest is the same as recommended in the Scala JS documentation for their demo project.

We then create our application in Scala and save it:

// client/src/main/scala/client/App.scala
package client

import scala.scalajs.js
import scala.concurrent.ExecutionContext.Implicits.global
import js.Thenable.Implicits.*
import org.scalajs.dom
import io.circe.syntax.*
import io.circe.generic.auto.*
import io.circe.parser.parse

// We redefine the Product class here. This can be done using a shared project
// but we keep it that way for simplicity reasons.
case class Product(code: String, description: String, price: Double)

@main
def HelloWorld(): Unit =
  println("Hello World!")
  val responseText = for {
    response <- dom.fetch("http://localhost:5173/api/products")
    text <- response.text()
  } yield {
    text
  }
  val app = dom.document.getElementById("app")
  for {
    text <- responseText
  } yield {
    val products = parse(text).flatMap(_.as[List[Product]]).toOption.get
    app.innerHTML =
      """<table class="table"><thead><tr><th>Code</th><th>Desc</th><th>Price</th></tr></thead><tbody>""" + products
        .map(x =>
          s"<tr><td>${x.code}</td><td>${x.description}</td><td>${x.price}</td></tr>"
        )
        .mkString + "</tbody></table>"
  }

We are now ready to compile it with sbt, from the root directory of the project (not client, but the full project) run:

$ sbt client/compile 

This compiles the client but you cannot still use the code. You can use Vite.js for that.

Configuring Vite.js to use Scala JS

You need now to add the ScalaJS plugin to Vite.js, this is done with the following command (from the client project directory):

$ npm install -D @scala-js/vite-plugin-scalajs@1.0.0

Finally, create the vite.config.js file and fill it with the following content:

import { defineConfig } from "vite";
import scalaJSPlugin from "@scala-js/vite-plugin-scalajs";

export default defineConfig({
    base: "/",
    plugins: [scalaJSPlugin(
        {
            cwd: "..",
            projectID: "client"
        }
    )],
    server: {
        proxy: {
            '/api': 'http://localhost:8080',
        },
    },
});

Now, you can open two terminals and in the first one run:

$ sbt server/run

This will run the server in the background on port 8080.

In the second:

$ npm run dev

This will run the application on http://localhost:5173/.

The Vite.js ScalaJS plugin will compile and run the Scala code for the client directly using sbt and you can see the new application in your browser.

The Vite.js configuration also includes a proxy that redirects requests from the client to the actual server. This is required because browser security features usually will not allow Javascript code in a website to call Javascript in another website.

Setting up auto-reload

We stated at the beginning that we wanted auto-reload both in the client and in the server. For the client, there is nothing else to do. You just need to launch the command:

$ sbt ~client/fastLinkJS

It will keep updating the client’s Javascript as you modify it and Vite.JS will find the modifications and update the view.

For the server, you need to put a bit extra effort. Indeed, the problem with the server is that when you launch the run command it will stay there running the server and it will not restart when you change the code. To get sbt to restart the server you need to use the sbt-revolver plugin. Just go to your plugins.sbt file and add the following line:

addSbtPlugin("io.spray" % "sbt-revolver" % "0.10.0")

You can now launch the server with:

$ sbt ~server/reStart

This will restart the server each time the code is changed.

Conclusions

In this tutorial we have shown the simplest app possible that includes both a client and a server in Scala. I hope you enjoyed it.

If you have read until here and liked this content please consider subscribing to my mailing list. I write regularly about Blockchain Technology and Scala.

comments powered by Disqus