Running and deploying a modern web application can be a complicated process, involving steps such as minification, bundling code together, spinning up infrastructure, connecting to cloud services etc. SAFE Stack makes this easy with some built-in tools and packages that provide:
- A simple local development cycle with hot-reload for the full stack application.
- Simple infrastructure-as-code for deploying the application directly to Microsoft Azure.
Local Development
A key part of developing any SPA is to simplify running your local application, which invariably contains two different components:
- The "back-end" server. This is typically a .NET application running Saturn / Giraffe / ASP .NET.
- The "front-end" application. Although this is actually just transpiled JavaScript, SAFE uses Vite to bundle and host the client through a local development server.
Running both of these to talk together can be a pain to configure by hand, especially with a complex JS build chain. In SAFE Stack, we provide a dedicated runner application which automates the configuration, build and running of the application. The below (compressed) output shows both server and client being built and run in parallel from a single dotnet run
command.
dotnet run
Shortened Dependency Graph for Target Run:
<== Run
<== Clean
Starting target 'Clean'
"dotnet" fable clean --yes
Fable 4.19.3: F# to JavaScript compiler
Minimum @fable-org/fable-library-js version (when installed from npm): 1.4.2
Deleted output\fable_modules
Clean completed! Files deleted: 0
Finished (Success) 'Clean' in 00:00:00.3717008
Starting target 'Run'
server: dotnet watch run --no-restore
client: dotnet fable watch -o output -s --run npx vite
client: Fable 4.19.3: F# to JavaScript compiler
client: Parsing Client.fsproj...
client: .> cmd /C dotnet restore Client.fable-temp.csproj -p:FABLE_COMPILER=true -p:FABLE_COMPILER_4=true -p:FABLE_COMPILER_JAVASCRIPT=true
server: dotnet watch ?? Started
server: Now listening on: http://localhost:5000
server: Application started. Press Ctrl+C to shut down.
client: Started Fable compilation...
client: Fable compilation finished in 6234ms
client: .> cmd /C npx vite
client: Watching ..
client: VITE v5.0.13 ready in 211 ms
client: ? Local: http://localhost:8080/
client: ? Network: use --host to expose
client: ? press h + enter to show help
Notice the mention of "Dependency Graph". This comes from FAKE - an F# DSL for build tasks. Each task (such as Clean or Run) are actually functions that execute specific activities, such as cleaning out a folder or compiling code. These can then be chained together into a dependency graph through a simple DSL:
let dependencies = [
"Clean" ==> "RestoreClientDependencies" ==> "Bundle" ==> "Azure"
"Clean" ==> "RestoreClientDependencies" ==> "Run"
"RestoreClientDependencies" ==> "RunTests"
]
In the above example, we specify that in order to carry out the "Run" build task, we must first execute RestoreClientDependencies, which itself is dependent on Clean. When running the entire build chain, FAKE will automatically orchestrate the dependencies as required. In addition to function dependency management, FAKE also comes with a vast library of helper functions to make common build tasks easy to do:
And because FAKE is just a set of F# packages, you can use it and extend it however you wish - whether within a running application or as part of a script.
FAKE also comes with it's own "runner", that allows you to run FAKE scripts directly from .fsx files. However, advancements in the .NET CLI have reduced the need for this. SAFE's build application is a fully-fledged .NET Console application, which allows you to use standard
dotnet run
syntax to start your SAFE application.
Deployment to the Cloud
Deploying your application to a production environment is a separate task from what we've just seen. Instead of hosting two components, you'll need to "bundle" up the application into a single executable that can handle standard API requests, but can also serve up assets such as minified JS. The SAFE build application comes with a specific Bundle target that does exactly this. With this output, you can deploy this to any platform that can host a .NET application and expose HTTP/S endpoints.
If you want to use Microsoft Azure's App Service, you're in luck, as the template comes with a ready-made, fully automated deployment from scratch. SAFE uses the Farmer project, which is an easy-to-learn library for rapidly authoring and deploying entire Azure architectures. Farmer transpiles down to standard ARM JSON, so all the standard Azure deployment mechanisms such as Azure CLI work out-of-the-box with Farmer. The following FAKE target defines all the infrastructure for a SAFE Stack app running on Linux, creates all the infrastructure (app service, web app and fully configured application insights logging) from scratch in an idempotent fashion and then securely copies the bundled application into the web application.
Target.create "Azure" (fun _ ->
let web = webApp {
name "SAFEDemoWebApp"
operating_system Linux
runtime_stack Runtime.DotNet80
zip_deploy "deploy"
}
let deployment = arm {
location Location.WestEurope
add_resource web
}
deployment
|> Deploy.execute "SAFE-Demo-App" Deploy.NoParameters
|> ignore
)
Farmer can go much further, like securely linking together multiple resources without having to pass secrets insecurely and correctly configuring dependencies. On top of that, since Farmer is "just F#", you can seamlessly tie it into any of your existing applications and extend it as needed. It's also extremely easily to parameterise Farmer templates - you can easily wrap them in a function and pass in any arguments required for parameterisation:
let createInfrastructure (env: string) =
let blobStorage = storageAccount {
name $"SAFEDemoStorage{env}" // parameterise the name of the storage account
}
let web = webApp {
name $"SAFEDemoWebApp{env}"
setting "storage-key" blobStorage.Key // securely set the storage key as an app setting
Summary
SAFE Stack comes with all the key parts that you need to quickly run and deploy full-stack F# web applications. As with much of SAFE Stack, we haven't built technologies from the ground up - instead we've harnessed existing tools, libraries and frameworks wherever possible. Not only has this saved time, but it also ensures a more consistent experience and encourages further development of existing tools within the ecosystem.