Skip to main content

Fullstack app with Elmish Land

Introduction​

If you're a fan of F#, you probably want to write both your backend and frontend in your favorite language. In this guide, I'll walk through how to build a fullstack web application using:

  • ASP.NET Web API in F# for the backend. I will use raw ASP.NET in this tutorial but other frameworks are also supported eg. Giraffe or Saturn.
  • Elmish Land for the frontend
  • A shared F# library for types used across the stack

Elmish Land is a delightful way to write frontends in F# using Elmish. It's perfect for those who want to keep the type safety and functional programming style consistent across the stack.

Let’s dive in!

πŸ’‘ Note: Another popular approach to fullstack F# development is the exellent SAFE stack and SAFEr template. The key difference with this guide is its use of Elmish Land, which introduces additional features such as a file-based router and type-safe routing out of the box.

Project Setup​

Let’s start from scratch and get everything wired up.

Create the Project Directory​

mkdir ElmishLandFullStack
cd ElmishLandFullStack
mkdir Client Server Shared
dotnet new tool-manifest

This creates a top-level directory for your fullstack app with directories for the Client, Server, and Shared projects.

Shared Project – Common Types​

The Shared project will hold types used in both the frontend and backend.

cd Shared
dotnet new classlib -lang f#

Server Project – ASP.NET Web API​

Now set up the backend:

cd ../Server
dotnet new webapi -lang f#
dotnet add reference ../Shared/Shared.fsproj

We reference the Shared project here so the backend can use shared types.

Client Project – Elmish Land Frontend​

Initialize the Elmish Land frontend:

cd ../Client
dotnet tool install elmish-land
dotnet tool install fable
dotnet elmish-land init
# We will use the top level dotnet tool configuration
# directory instead of this
rm -rf .config
dotnet add package Thoth.Fetch
dotnet add reference ../Shared/Shared.fsproj

You now have a working Elmish Land frontend app with F# and Fable. We also add Thoth.Fetch for HTTP requests and reference our shared types.

Create a Solution File​

Let’s group everything into a solution so IDEs like Visual Studio or Rider can play nice.

cd ..
dotnet new sln
dotnet sln add Shared/Shared.fsproj
dotnet sln add Server/Server.fsproj
dotnet sln add Client/Client.fsproj
dotnet sln add Client/.elmish-land/Base/ElmishLand.Client.Base.fsproj
dotnet sln add Client/.elmish-land/App/ElmishLand.Client.App.fsproj

Add package.json for Development Scripts​

To make development easier, let’s add a package.json to run both client and server in watch mode.

{
"workspaces": [
"Client"
],
"scripts": {
"dev-server": "dotnet run --watch --project Server/Server.fsproj",
"dev-client": "npm start --workspace=Client",
"start": "concurrently --kill-others \"npm run dev-server\" \"npm run dev-client\""
}
}

Install the required npm dependency:

npm i concurrently --save-dev

Consuming Web API Data from Elmish Land​

Move Shared Types to the Shared Project​

Let’s define a type used by both frontend and backend.

  1. Create a file Shared/WeatherForecast.fs

    namespace Shared

    open System

    type WeatherForecast =
    { Date: DateTime
    TemperatureC: int
    Summary: string }

    member this.TemperatureF =
    32.0 + (float this.TemperatureC / 0.5556)

  2. Update the project file Shared.fsproj

    Remove the default Library.fs file and include your new file:

    rm Shared/Library.fs
    <!-- Shared/Shared.fsproj -->
    <ItemGroup>
    <Compile Include="Library.fs" />
    <Compile Include="WeatherForecast.fs" />
    </ItemGroup>
  3. Remove WeatherForecast.fs from the server project

    rm Server/WeatherForecast.fs

    and update the project file:

    <!-- Server/Server.fsproj -->
    <ItemGroup>
    <Compile Include="WeatherForecast.fs" />
    <Compile Include="Controllers/WeatherForecastController.fs" />
    <Compile Include="Program.fs" />
    </ItemGroup>

Update the Backend Controller​

Change the open in Server/Controllers/WeatherForecastController.fs so we can use the new shared type:

namespace Server.Controllers

open System
open Microsoft.AspNetCore.Mvc
open Microsoft.Extensions.Logging
open Server
open Shared

...

Enable CORS in Program.fs​

Add CORS configuration to Program.fs to enable calling the API from the Client.

// Server/Program.fs

namespace Server
#nowarn "20"
open System
open Microsoft.AspNetCore.Builder
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Hosting

module Program =
let exitCode = 0

[<EntryPoint>]
let main args =

let builder = WebApplication.CreateBuilder(args)

builder.Services.AddControllers()

builder.WebHost.ConfigureKestrel(fun _ serverOptions ->
// Ensures that we always get the correct
// port used by the client
serverOptions.Listen(Net.IPAddress.Loopback, 5001)
)

builder.Services.AddCors(fun options ->
options.AddDefaultPolicy(fun policy ->
policy
.SetIsOriginAllowed(fun origin -> Uri(origin).Host = "localhost")
|> ignore
)
)

let app = builder.Build()

app.UseHttpsRedirection()

app.UseCors()
app.UseAuthorization()
app.MapControllers()

app.Run()

exitCode

Calling the API from the Frontend​

Replace the file Client/Pages/Page.fs with the following:

// Client/Pages/Page.fs

module Client.Pages.Page

open Feliz
open ElmishLand
open Client.Shared
open Client.Pages
open Thoth.Json
open Thoth.Fetch
open Shared
open Fable.Core.JS

type Model = {
WeatherForecasts: WeatherForecast array
}

type Msg =
| LayoutMsg of Layout.Msg
| WeatherForecastFetched of WeatherForecast array

let fetchWeatherForecast (): Promise<WeatherForecast array> =
Fetch.get("http://localhost:5001/WeatherForecast", caseStrategy = CamelCase)

let init () =
{ WeatherForecasts = [||] },
Command.ofPromise fetchWeatherForecast () WeatherForecastFetched

let update (msg: Msg) (model: Model) =
match msg with
| LayoutMsg _ -> model, Command.none
| WeatherForecastFetched weatherForecasts ->
{ WeatherForecasts = weatherForecasts }, Command.none

let view (model: Model) (_dispatch: Msg -> unit) =
Html.table [
Html.thead [
Html.tr [
Html.th "Summary"
Html.th "Date"
Html.th "TemperatureF"
]
]
Html.tbody [
for weatherForecast in model.WeatherForecasts do
Html.tr [
Html.td weatherForecast.Summary
Html.td (weatherForecast.Date.ToShortDateString())
Html.td weatherForecast.TemperatureF
]
]
]

let page (_shared: SharedModel) (_route: HomeRoute) =
Page.from init update view () LayoutMsg

Now you can just run npm start to fire up both server and client!

Conclusion​

You now have a working fullstack F# application with:

  • ASP.NET Web API for your backend logic
  • Elmish Land for a reactive frontend experience
  • A shared library of F# types for type-safe communication

Elmish Land keeps the frontend functional and composable, making it a joy to build UIs. Combined with the robust ASP.NET ecosystem on the backend, you’ve got the best of both worlds β€” and all in F#!

Next Steps​