In this tutorial we guide you through the process of setting up a web application with:
- ScalaJS on the front-end
- Scala (akka-http) on the back-end
- Shared code between the front and back-end (isomorphism)
You'll build it from the ground-up and we keep things as simple as possible. You will need a web browser (duh) and sbt installed (https://www.scala-sbt.org/), any version higher than 0.13.16 will do (we are using 1.1.6)
Create the project
Download the directory structure and scalajs-crossproject-tutorial.tar.gz
Extract it somewhere on your computer and notice the following directory structure:
scala-series/
├── index.html
├── project
│ ├── build.properties
│ └── plugins.sbt
├── front-end
│ └── src
│ └── main
│ └── scala
├── back-end
│ └── src
│ └── main
│ └── scala
└── shared
└── src
└── main
└── scala
Only the following files have contents:
index.htmla empty html pageproject/build.propertiesthe version number of sbt (1.1.6)
Front-end
We start by quickly getting scalajs up and running.
First add the scalajs plugin to your project/plugins.sbt:
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.23")
Let's define our project inside our build file:
lazy val frontEnd = (project in file("front-end"))
.enablePlugins(ScalaJSPlugin)
.settings(
scalaVersion := "2.12.6",
scalaJSUseMainModuleInitializer := true,
libraryDependencies ++= Seq(
"org.scala-js" %%% "scalajs-dom" % "0.9.2",
"com.typesafe.play" %%% "play-json" % "2.6.0",
)
)
For those less familiar with scalajs:
- We are enabling the scalajs plugin you added to the plugins of this project
- we are using
scalaJSUseMainModuleInitializeras we want to build a front-end application as apposed to a scalajs/javascript library - We are adding
scalajs-domso that we can used the DOM api using types - Later on we will be using
play-json, we are just already defining it like we did for the back-end
Cool, now let's define a application 'demo.fe.SampleFrontendApp'
import org.scalajs.dom
import org.scalajs.dom.Event
object SampleFrontendApp {
def main(args: Array[String]): Unit = {
dom.window.addEventListener("load",initApplication)
}
def initApplication(e: Event) = {
println("initialising application…")
displayTitle("The Americans")
displayTitle("Vinyl")
}
def displayTitle(title: String) =
dom.document.getElementById("titles")
.insertAdjacentHTML("beforeend",s"<li>$title</li>")
---
So we are registering an onload event handler in which we log to the console and we display two series titles using
displayTitle. This helper function adds an li to a ul element that we will add to our HTML page later. Again we are just making something
visible so we can quickly get everything working so that we add "complexity" to the application.
Let's compile the scalajs application using sbt (and keep watching the files for recompilation)
$ sbt ~frontEnd/fastOptJS
This should produce a javascript file named front-end/target/scala-2.12/frontend-fastopt.js. Brilliant let's then now change the page so that we can test our
scalajs code
The Page
Open the index.html . You should already find a html5 template (created with emmet).
First we add some content to the page including our ul element with id titles:
<body>
<section>
<h1>Series</h1>
<ul id="titles">
</ul>
</section>
<footer>(fastopt version)</footer>
</body>
Now include our compiled javascript to the page (inside the head element)
<head>
…
<script src="front-end/target/scala-2.12/frontend-fastopt.js"></script>
</head>
You should be able to open your html page inside a browser and notice the titles are shown. Also notice the log message in your browser's development console.
That works, so let's now work on our backend
The backend
For the back-end we will be using
akka-actor,akka-httpandakka-slf4jakka-http-circeandakka-http-play-jsonlater on for the json marshalling "", Let's define the project inside our 'build.sbt':
lazy val backEnd = (project in file("back-end"))
.settings(
resolvers += Resolver.bintrayRepo("hseeberger", "maven"),
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-actor" % "2.5.11",
"com.typesafe.akka" %% "akka-http" % "10.1.0",
"de.heikoseeberger" %% "akka-http-circe" % "1.20.0",
"de.heikoseeberger" %% "akka-http-play-json" % "1.20.0",
"com.typesafe.akka" %% "akka-slf4j" % "2.5.11",
)
)
We will place the actual akka-http route inside a trait named demo.be.SeriesService. Create this trait and define a route:
trait SeriesService {
lazy val seriesApi =
path("api" / "series") {
respondWithHeaders(`Access-Control-Allow-Origin`.*) {
get {
complete("McMafia")
}
}
}
---
We will change the route later, but we are just trying to get things rolling as quick as possible. For those who are not familiar with akka-http:
- We are using the akka-http DSL to define a route
- When a request comes in rules (in the form of directives) determine what will be returned
- In our case we have a match for a URI path
/api/series - Any http request send to that uri will have a CORS header allowing any host to use our endpoint
- When the request method is a http GET, we reply with the text "hello friend"
A quick note on the CORS header allowing any host: You would normally never do that, but in our case we will test our application by just opening a html file from the filesystem. Normally you would allow just the host/port used to serve your web resources.
We can now define our application. Create a new object named demo.be.SampleServiceApp and make it a Scala application by extending App plus
mixin our SeriesService trait
object SampleServiceApp extends App with SeriesService {
---
We then define the necessary implicits and start our server with our route rules:
implicit val system = ActorSystem("my-system")
implicit val materializer = ActorMaterializer()
implicit lazy val ex: ExecutionContext = system.dispatcher
Http().bindAndHandle(seriesApi, "0.0.0.0", 3000) onComplete {
case Success(b) => println(s"server is running ${b.localAddress} ")
case Failure(e) => println(s"there was an error starting the server $e")
---
This will open port 3000 on all local IP addresses and use our seriesApi route to service requests.
Run your application using sbt
$ sbt backEnd/run
Quickly test your endpoint by sending a GET request to http://localhost:3000/api/series either in your browser or using curl
$ curl http://localhost:3000/api/series
McMafia
Use our endpoint
So time to update our front-end code and make a XHR request to our server.
Add a method getData to make the XHR request using scalajs-dom's extension
Ajax.get
def getData =
Ajax.get("http://localhost:3000/api/series")
.map(_.responseText)
We can then use this in our initApplication method
import scala.concurrent.ExecutionContext.Implicits.global
getData.onComplete{
case Success(v)=>displayTitle(v)
case Failure(e)=>println(s"oops: $e")
---
Upon successful completion we display the title and in the event of a failure we write to the console.
You are most likely still running the triggered execution (sbt ~frontEnd/fastOptJS) so you should just have to
refresh your browser and you should see the series your server is sending.
Top! time to add some json and a shared scala model representing a series.
The Cross Platform Project
This is where it will become really cool. We are going to define a cross project using sbt-crossproject for our model classes.
The sbt-crossproject project allows "Cross-Compiling Scala.js, JVM and Native".
Add the required plugin to your projects/plugins (in this case we need cross-compiling for JS and the JVM)
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.4.0")
We can now define a JS/JVM cross project for our shared model:
lazy val shared = crossProject(JSPlatform, JVMPlatform)
.crossType(CrossType.Pure)
.settings(
libraryDependencies ++= Seq(
"com.typesafe.play" %% "play-json" % "2.6.0",
)
)
The crossType needs explanation. This setting determines
the source tree layout of the cross project. We are using Pure which means there will be no directories for
platform-specific code. All the code will be placed in the src folder and is considered "shared" between scalajs and the jvm
compilation. Another setting would be Pure which creates
source directories such as js, jvm and shared. We have no platform-specific code, so only need a single src
folder.
Next we can define platform specific sbt Projects and add those as dependencies to our front- and back-end projects:
lazy val sharedJVM = shared.jvm
lazy val sharedJS = shared.js
lazy val frontEnd = project
.dependsOn(sharedJS)
…
lazy val backEnd = project
.dependsOn(sharedJVM)
…
Pretty cool, no? Let's define our model classes next.
Json Model classes
Within the shared project add a demo.shared.Series case class
case class Series(id: String, title: String)
We will now define json support for our model. We have already added the dependencies to our projects. As you might
have noticed we will use Play's Json support. We have also
added akka-http-json and akka-http-play-json so that we can use play-json with akka-http. The reason we are using play-json and not
akka's own json support is to use one json configuration for both the front and back-end (akka json does not support scalajs)
This means we can define our marshaller inside the shared project! Awesome (it is really).
Again inside the shared project add a trait named demo.shared.JsonSupport and define the Format implicit for our Series case
class:
import play.api.libs.json.Json
trait JsonSupport {
implicit val SeriesFormat = Json.format[Series]
---
Next we need to change the back- and front-ends.
Change the Back-end
We start with the back-end.
Open the demo.be.SeriesService and mixin our JsonSupport plus the PlayJsonSupport so that akka-http can
use Play's json support:
import demo.shared.JsonSupport
import de.heikoseeberger.akkahttpplayjson.PlayJsonSupport
import demo.shared.Series
trait SeriesService extends JsonSupport with PlayJsonSupport {
…
---
Then just change the response value to return a sequence of Series (which will be marshaled into a json array)
get {
complete(Seq(Series("8219","Sense8"), Series("9214","Better Call Saul")))
---
If you are still running the previous server, make sure you stop it and recompile/run your new version
$ sbt backEnd/run
Quickly test your updated endpoint either in your browser or using curl
$ curl http://localhost:3000/api/series
[ {
"id" : "8219",
"title" : "Sense8"
}, {
"id" : "9214",
"title" : "Better Call Saul"
} ]
Time to update the front-end
Update the front-end
Make sure you restart the triggered execution as we have updated the build file
$ sbt ~frontEnd/fastOptJS
Then open your demo.fe.SampleFrontendApp and mixin our json support class:
import demo.shared.{JsonSupport, Series}
object SampleFrontendApp extends JsonSupport {
---
Then change the getData method to parse (Json.parse) and unmarshal (as[Seq[Series]]) the response from our service call:
def getData : Future[Seq[Series]] =
Ajax.get("http://localhost:3000/api/series")
.map(_.responseText)
.map(Json.parse(_))
.map(_.as[Seq[Series]])
As a final step change the initApplication and call displayTitle for each series returned
getData.onComplete{
case Success(v)=>v.foreach(s=>displayTitle(s.title))
case Failure(e)=>println(s"oops: $e")
---
Refresh your page and "Wubba Lubba dub-dub, it works!" (well in excitement and not because you are in pain)
You can find the solution of this tutorial on github