F# Options for Building Web Applications
F# can build full web applications on top of ASP.NET Core in several ways: writing ordinary ASP.NET Core controllers or minimal API endpoints directly in F#, using Giraffe — a lightweight functional web framework that models request handling as composable functions — or using Saturn, which layers a higher-level, more opinionated DSL on top of Giraffe for teams who want an MVC-like structure. On the frontend side, Fable compiles F# to JavaScript, letting teams share domain types and validation logic between server and browser instead of maintaining duplicate models in two languages.
Cricket analogy: A cricket academy might let a batter train under the standard national coaching curriculum, a specialized spin-bowling clinic, or a full boarding program with structured routines — three different levels of scaffolding for the same goal, just as F# web development offers plain ASP.NET Core controllers, the lightweight Giraffe framework, or the more structured Saturn framework.
Giraffe: Functional HTTP Handlers
Giraffe represents each unit of request handling as an HttpHandler, a function of type HttpFunc -> HttpContext -> HttpFuncResult, and composes handlers with the >=> (Kleisli composition) operator so that route "/users" >=> GET >=> handleGetUsers reads as 'match this route, then this verb, then run this handler,' short-circuiting to the next branch if any step fails to match. This composition-first design means routing, content negotiation, authentication checks, and business logic are all just functions combined with the same operator, avoiding the inheritance-heavy controller base classes that traditional ASP.NET Core MVC relies on.
Cricket analogy: A fielding captain sets a field plan as a sequence of conditions — if it's a left-hander, then move point squarer, then check for the reverse sweep — short-circuiting to the next check if the batter isn't left-handed, exactly like Giraffe's route "/users" >=> GET >=> handleGetUsers chaining match-then-match-then-handle.
Saturn: A Structured Framework on Giraffe
Saturn adds computation expressions like application { }, router { }, and controller { } on top of Giraffe's primitives, giving teams a more familiar, convention-based structure — a controller { } block can define index, show, create, and update members that map to RESTful routes automatically, similar to Rails or ASP.NET Core MVC controllers but expressed as F# computation expressions rather than class inheritance. This makes Saturn a good fit for teams migrating from MVC-style frameworks who still want F#'s type safety and Giraffe's composable HttpHandler model underneath.
Cricket analogy: A franchise league that adopts the BCCI's standard tournament format — fixed group stages, playoff brackets, and net-run-rate tiebreakers — instead of inventing its own rules from scratch mirrors how Saturn's controller { } gives F# developers RESTful conventions (index, show, create, update) instead of manually wiring every Giraffe route.
Fable and Shared Domain Types
Fable compiles a subset of F# to readable JavaScript, and the most common pattern pairs it with the Elmish library implementing the Model-View-Update (MVU) architecture popularized by Elm: a Model type, an update: Msg -> Model -> Model function, and a view: Model -> Dispatch<Msg> -> ReactElement function drive the whole UI as a pure, testable state machine. Because both the Fable frontend and the ASP.NET Core/Giraffe backend are F#, a single project can define a DTO or validation function once in a shared project and reference it from both client and server code, eliminating an entire class of drift between frontend and backend models.
Cricket analogy: A single official scorecard format used identically by the stadium's big screen, the TV broadcast graphics, and the official app avoids three different teams maintaining three different scoring systems, just as a shared F# project lets both the Fable frontend and the Giraffe backend reference the exact same DTO definitions.
open Giraffe
open Microsoft.AspNetCore.Http
type User = { Id: int; Name: string }
let getUserHandler (id: int) : HttpHandler =
fun (next: HttpFunc) (ctx: HttpContext) ->
task {
let user = { Id = id; Name = "Ada Lovelace" }
return! json user next ctx
}
let webApp : HttpHandler =
choose [
route "/health" >=> GET >=> text "OK"
routef "/users/%i" (fun id -> GET >=> getUserHandler id)
RequestErrors.NOT_FOUND "Not found"
]Giraffe's HttpHandler is 'just a function,' which means you can unit test a handler by calling it directly with a fake HttpContext and asserting on the response — no in-memory test server or web host required for most business-logic tests, unlike testing a traditional MVC controller action in isolation.
Fable does not compile arbitrary .NET libraries to JavaScript — only F# source (and a curated set of Fable-compatible packages) can run in the browser. Reaching for a server-only library like Entity Framework Core or System.IO.File inside Fable code will fail to compile, so shared projects should stick to plain data types and pure functions with no BCL I/O dependencies.
- F# can build ASP.NET Core apps directly, or via Giraffe (functional, composable HttpHandlers) or Saturn (higher-level DSL on top of Giraffe).
- Giraffe's HttpHandler is a plain function composed with the >=> operator for routing, verb matching, and business logic.
- Saturn's controller { } and router { } computation expressions give a convention-based, MVC-like structure over Giraffe's primitives.
- Fable compiles F# to JavaScript, commonly paired with Elmish's Model-View-Update (MVU) architecture.
- A shared F# project can define DTOs and validation logic once, referenced by both a Fable frontend and a Giraffe/ASP.NET Core backend.
- Giraffe handlers can be unit tested as plain functions without spinning up an in-memory web host.
- Fable only compiles F# source and Fable-compatible packages — server-only BCL APIs like File I/O won't compile for the browser.
Practice what you learned
1. What is a Giraffe HttpHandler at its core?
2. Which operator does Giraffe use to compose HttpHandlers together?
3. What does Saturn add on top of Giraffe?
4. What architecture does Fable commonly pair with via the Elmish library?
5. Why can't a Fable frontend project reference Entity Framework Core?
Was this page helpful?
You May Also Like
Asynchronous Programming in F#
F#'s Async<'T> type and async { } computation expression provide a composable, cold-start model for asynchronous work that predates and interoperates with .NET's Task-based async/await.
F# and .NET Interop
F# runs on .NET and can freely call into C# libraries and the Base Class Library, and be consumed from C# projects — understanding the interop rules is essential for real-world F# work.
Testing F# Code
F#'s pure, immutable style makes much business logic naturally testable, and frameworks like Expecto and FsCheck lean into that style with lightweight, value-based test suites and property-based testing.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics