Skip to main content

Fullstack F# with Elmish Land and ASP.NET

· 6 min read
Kristofer Löfberg
Elmish Land creator

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