INTRODUCTION
A lot of modern day web applications have shifted towards making "single page applications". Many responsibilities that used to only fall to the server are now also incumbent upon the browser client. Not to mention that complex and demanding applications such as dashboards are also run in the browser these days.
This has a lot of consequences. For example, a lot of data models and business logic must be available to both the back end and the front end. Validation of these models is performed in both places. And the complexity of the application requires to have a solid and consistent code base. In that regard, writing JavaScript with your bare hands is no more a viable option. In the back end as well, things are evolving. People are expecting responsive websites and no downtime (otherwise they will swiftly move to other places).
Is there something out there that can help you solve all these challenges? There is at least one that can, and it is Scala.
Scala is a powerful language running on the JVM (thus sharing the unbeatable Java ecosystem) which can also be compiled into plain JavaScript and its door owards running inside the browser. Having the same language in the back end and front end solves the problem of sharing logic. Moreover, Scala was thought from its foundations to be tailored for solving complex domain problems and architecture.
In the following, we are going to review possible project choices which allow you to leverage Scala's capabilities.
PROJECT STRUCTURE
A "full stack" Scala project can typically be divided into three sub-projects: one for the backend, one for the frontend, and one for the part of the code that you will share between these two. From the shared project comes the power of the choice of technology: you'll only write code once, and you will receive the guarantee that your code does the exact same thing on your server and in your browsers. Scala basically highjacked Java's first slogan "write once, run everywhere" although in a much more powerful way.
Your folder structure will look like this[1]:
├── build.sbt # Configuration file for orchestrating your project
├── /backend/ # Source and resource files for your server
│ ├── /src/main/scala
│ └── /src/main/resources
├── /frontend/ # Source and resource (asset) files for your front end
│ ├── /src/main/scala
│ └── /src/main/resources
├── /shared/ # Source files for both your front end and back end
| └── /src/main/scala
├── /project/ # More configuration, such as dependencies or plugins
Both your front end and your back end are able to use the source files in the shared
directory. The build.sbt
file will be the place where you define your three sub-projects, as well as their inter-dependencies. It can also be the place where you define involved tasks to automate parts of your development process.
A minimal build.sbt
example would be
import sbt.Keys._
import sbtcrossproject.CrossPlugin.autoImport.{crossProject, CrossType}
scalaVersion := "2.13.1"
lazy val `shared` = crossProject(JSPlatform, JVMPlatform)
.crossType(CrossType.Pure) // this will cross compile
lazy val `backend` = (project in file("./backend")) // this will run on the jvm
.dependsOn(shared.jvm)
lazy val `frontend` = (project in file("./frontend"))
.enablePlugins(ScalaJSPlugin) // this where we say that frontend compiles to JavaScript
.settings(scalaJSUseMainModuleInitializer := true) // project with a main method, not a library (roughly)
.dependsOn(shared.js)
and the only thing that you need to do to enable that is adding the line addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.0.1")
to the project/plugins.sbt
file.
A SHARED MODEL
In our small project, we want to allow the users to register to our website. The minimal needed information to be able to register would be a name
, an email
address and a password
(possibly confirmed). This data will be sent as JSON from the client to the server. Obviously, we want to validate the sanity of the model in the front end (so that we can provide immediate feedback to the user) and in the back end (because we can't trust the client, and because there is some validation that the front end can't do, e.g., knowing whether that name already exists).
Scala has a special syntax for defining data:
case class RegisterInfo(name: String, email: String, password: String, confirmPassword: String)
You could go further in type safety by using value classes (i.e., case classes with only one parameter) or a library like tagging. Now we can do basic verification that the contents of the RegisterInfo
are valid, as methods of the class:
case class RegisterInfo(name: String, email: String, password: String, confirmPassword: String) {
def isNameValid: Boolean = name.nonEmpty
def isEmailValid: Boolean = email.contains("@") // some basic stuff
def isPasswordValid: Boolean = password.nonEmpty && password == confirmPassword
def isValid: Boolean = isNameValid && isEmailValid && isPasswordValid
}
This model will be available everywhere, and using a JSON serialization library like circe or upickle, we can communicate this model from the frontend to the backend, being sure that it will always be in sync.
THE SHARED "FRAMEWORK"
Besides sharing models and their validations, Scala also provides amazing libraries for fully embracing functional programming style. This is obviously not mandatory, and Scala can as well be used as a "better Java". But if you go the functional route, you have an array of very good tools at your disposal, e.g., cats-effect, scalaz or ZIO. All these libraries allow you to have a functional, easily-tested and consistent code base among your entire project.
THE BACK END
Scala has a lot of high quality web frameworks available, like play, cask, akka-http or http4s, all coming in different flavors.
The back end also requires you to communicate with some persistent layer. Once again, Scala has plenty of choices for you, especially for relational databases: slick, doobie, quill... For example, using slick, we can imagine having a table users
filled with instances of case class User(id: Long, name: String, email: String, hashedPassword: String)
We can now write a route that will register the new user in the users table. Writing this route (in any of the afore mentioned web frameworks) in "vanilla" Scala would look like
if (!registerInfo.isValid) Future.successful(BadRequest("not valid"))
else (for {
userExists <- db.run(users.filter(user => user.name === registerInfo.name || user.email === registerInfo.email).result.headOption).map(_.isDefined)
_ <- if (userExists) throw new UserAlreadyExists else Future.successful(())
_ <- db.run(users += User(0L, registerInfo.name, registerInfo.email, hassPassword(registerInfo.password)))
} yield Ok)
.recover { case _: UserAlreadyExists => BadRequest("user exists") }
Using ZIO, in a world where you built your error AST around "http responses" (which is a little naive, I admit), this would perhaps look like
for {
_ <- if (!registerInfo.isValid) ZIO.failed(BadRequest("not valid")) else ZIO.unit
userExists <- database.findUserByNameOrEmail(registerInfo.name, registerInfo.email).map(_.isDefined)
_ <- if (userExists) ZIO.failed(BadRequest("user exists")) else ZIO.unit
_ <- database.insertUserFromRegisterInfo(registerInfo)
} yield Ok
Depending on the "shared framework" you chose, this can look very different, but the idea is there. Note also that we don't need to provide better feedback than "not valid", since the front end can do that also.
THE FRONT END
The final big component of our triplet is the front end. As we talked about single page applications, we need a framework to do that for us. As usual, choices are there. In JavaScript/TypeScript, the three main contenders are probably React, Angular and Vue. If you so desire, you can use one of these for your web application in Scala. However, despite some proof of concepts demonstrating that using Angular (or Vue) is possible, the preference in the Scala community clearly leaned towards React. But in Scala, even React comes in two different flavors: scala-js-react and Slinky. And there are even other choices that are written directly in Scala, as it is for example the case for Laminar.
Let's see a sample of what the form for filling the email in RegisterInfo would look like.
With Slinky, which is essentially React:
case class State(registerInfo: RegisterInfo)
def emailInputClass = if (state.registerInfo.isEmailValid) "success" else "error"
def handleEmailChange(event: SyntheticEvent[html.Input, Event]): Unit =
setState(state.copy(email = event.target.value))
input(
`type` := "email",
className := emailInputClass,
onChange := (handleEmailChange(_))
)
And in Laminar, which is centered around observables:
val registerInfoChangerBus = new EventBus[RegisterInfo => RegisterInfo] // receives all updates in the register info
def changeEmail(email: String): RegisterInfo => RegisterInfo = _.copy(email = email) // describe how email is updated
val emailObserver = registerInfoChangerBus.writer.contramap(changeEmail) // observe all changes in the email
val $registerInfo = registerInfoChangerBus.events.fold(RegisterInfo("", "", "", "")) {
case (registerInfo, changer) => changer(registerInfo)
} // emits the current RegisterInfo, starting from an empty one
val $emailHasError = $registerInfo.changes.map(!_.isEmailValid) // emits errors in the user input email
input(
`type` := "email",
className <-- $emailHasError.map(if (_) "error" else "success"), // feeding the css class with error or success
inContext(elem => onChange.mapTo(elem.ref.value) --> emailObserver) // changing the form content by feeding the email observer
)
USING JS LIBRARIES
When on the JVM, you can use any Java library out there. When you compile to JS, you can of course use any JavaScript (or TypeScript) library there is. Usual libraries like lodash or RxJS won't be needed of course (the Scala standard library is more comprehensive than lodash, and Scala-friendly observables are also available, e.g., monix). But from time to time, you will want to leverage the amazing set of JS libraries that exist. For example something like leaflet or pixi.
Just as you need typings when you use a JavaScript library from TypeScript, you need so-called facades in Scala. The ScalablyTyped project has you covered. ScalablyTyped compiles on demand TypeScript typings definition into Scala.js facades. You specify the npm dependencies that you need, and it does everything for you.
CONCLUSION
Going full stack in Scala is a safe bet. And a safe bet is a good investment. The level of quality of its libraries and frameworks is outstanding. And the language has the full power to model your most complex business problems without blinking an eye.
What's more, we didn't even scratch the surface of the layer behind your server. And this is the one where Scala shines even more, having access to a wide array of big data and distributed tools such as Kafka, Spark, Cassandra, or the Akka suite...
Above, we tried to present you a bunch of choices that you can make to build your perfect stack. If you were to ask for a personal advice, I would probably go for Laminar, Play, Slick and ZIO. But that is personal, and all combinations will give you full satisfaction.
If you would like to start playing with full stack applications in Scala, try the code from this article you can check here.