The preferable way would probably be to deploy your frontend application separately. But it's also a common practice to deploy backend and frontend apps as a single bundle (e.g. JHipster practice). Nevertheless, I can answer how to do it as bundled.
For convenience, you can create two modules in a single SBT project - service
and ui
(root directory for the frontend app). One for the backend and another one for the frontend.
SBT settings
Depending on the frontend framework you use, we will modify SBT settings a bit. Let's say we use React. By default, if we run npm build
or yarn build
, frontend bundled files will end up in ui/build
directory by default. We will mark build
directory as "resources" in our ui
module:
lazy val `ui` =
project
.in(file("./ui"))
.settings(
resourceGenerators in Compile += buildUi.init
)
lazy val buildUi = taskKey[Seq[File]]("Generate UI resources") := {
val webapp = baseDirectory.value / "build"
val managed = resourceManaged.value
for {
(from, to) <- webapp ** "*" pair Path.rebase(webapp, managed / "main" / "ui")
} yield {
Sync.copy(from, to)
to
}
}
service
module will depend on ui
module:
lazy val `service` =
project
.in(file("./service"))
.dependsOn(`ui`)
Now, service
can pick up resource files from ui
after you build your React application.
How to serve the frontend app alongside backend API
Let's presume that you created API routes that will be consumed by the frontend. Create a pathPrefix
route beginning with "api", "v1", "api/v1" or whatever, and you'll see later why do we need this prefix:
pathPrefix("api") { // api routes }
And create another route that will serve frontend assets:
def assets: Route =
getFromResourceDirectory("ui") ~
pathPrefix("") {
get {
getFromResource("ui/index.html", ContentType(`text/html`, `UTF-8`))
}
}
And then join two routes this way:
pathPrefix("api") seal { // api routes } ~ assets
Voila!
Let's explain these routes.
First, we want to match our API routes, since they are at the specific URL. assets
are matching all other URL's. This means, visiting any URL that doesn't start with /api
will return React's static resources.
Next, let's disect assets
routes:
First one is getFromResourceDirectory("ui")
. Remember when we marked ui/build
directory as resources directory? This means that our React resources are located in target/scala/classes/ui
directory and we can simply serve them this way.
The second one and the trickiest one:
pathPrefix("") {
get {
getFromResource("ui/index.html", ContentType(`text/html`, `UTF-8`))
}
}
This means that any URL that doesn't start with /api
will match this route and return React's index.html
file. But you might ask: "Why didn't we simply use pathSingleSlash
?" This is exactly the tricky part - frontend framework routing.
Let's say that we use pathSingleSlash
(we expose React's resources only on the root "/"). And let's say that we use e.g. react-router-dom
for routing in React application. And we have a nice "/users" route that displays users in a table. We go to "/" -> Akka HTTP server serves static files and all works as expected -> we click some button that goes to "/users" and all works as expected again since React router is doing routing now -> then we refresh our page, and we get a 404 error. Why? Because it's a known route in React app, but an unknown route in Akka HTTP server. Therefore, at any route except on API route ("/api") we want to expose our React resources. With pathPrefix("")
, we are doing exactly that. When we go to "/users" page and hit refresh, Akka HTTP still returns React's resources and the rest of the routing magic is done by React - "/users" page renders successfully.
You can also create CORS route so you can independently run frontend from backend during the development stage.
Happy hakking.