Web Development with Giraffe, Saturn, and Falco
Prerequisites: Async and task
F# web apps run on ASP.NET Core, the same fast, cross-platform web server C# uses. You can use ASP.NET Core directly, or one of the F# libraries that give it a functional style.
Minimal APIs, straight from F#
// illustrative: Program.fs in a project created with dotnet new web -lang F#
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Http
type Todo = { Id: int; Title: string; Done: bool }
let todos = [ { Id = 1; Title = "Learn F#"; Done = false } ]
let builder = WebApplication.CreateBuilder()
let app = builder.Build()
app.MapGet("/", System.Func<string>(fun () -> "Hello from F#!")) |> ignore
app.MapGet("/todos", System.Func<Todo list>(fun () -> todos)) |> ignore
app.MapGet("/todos/{id}", System.Func<int, IResult>(fun id ->
match todos |> List.tryFind (fun t -> t.Id = id) with
| Some todo -> Results.Ok todo
| None -> Results.NotFound())) |> ignore
app.Run()
$ curl http://localhost:5000/todos/1
{"id":1,"title":"Learn F#","done":false}
Giraffe
Giraffe composes HTTP handlers — functions that take a request
context and decide what to do — with the >=> ("fish") operator:
// illustrative: needs the Giraffe package and a web project
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Http
open Giraffe
type Todo = { Id: int; Title: string }
let todos = [ { Id = 1; Title = "Learn F#" }; { Id = 2; Title = "Build an API" } ]
let getTodo id : HttpHandler =
match todos |> List.tryFind (fun t -> t.Id = id) with
| Some todo -> json todo
| None -> RequestErrors.NOT_FOUND $"no todo {id}"
let webApp =
choose [
GET >=> route "/" >=> text "Hello from Giraffe"
GET >=> route "/todos" >=> json todos
GET >=> routef "/todos/%i" getTodo
RequestErrors.NOT_FOUND "Not found"
]
let builder = WebApplication.CreateBuilder()
builder.Services.AddGiraffe() |> ignore
let app = builder.Build()
app.UseGiraffe webApp
app.Run()
choosetries each handler in turn until one matches.routef "/todos/%i"parses the id as anintusing a typed format, likeprintf.>=>runs the next handler only if the previous one let the request through —GET >=> route "/"means "a GET request to /".- Giraffe.ViewEngine builds HTML with F# functions
(
div [] [ str "Hello" ]).
Saturn
Saturn builds on Giraffe with an opinionated, Rails/Phoenix-like
structure: application { }, router { }, and controller { }
computation expressions.
// illustrative: needs the Saturn package
open Saturn
open Giraffe
let appRouter =
router {
get "/" (text "Hello from Saturn")
getf "/hello/%s" (fun name -> text $"Hello, {name}")
}
let app =
application {
use_router appRouter
url "http://0.0.0.0:8085/"
}
run app
Falco
Falco is a lightweight toolkit focused on simplicity and speed, with its own routing and an HTML view engine:
// illustrative: needs the Falco package
open Falco
open Falco.Routing
open Microsoft.AspNetCore.Builder
let endpoints =
[ get "/" (Response.ofPlainText "Hello from Falco")
get "/json" (Response.ofJson {| Language = "F#"; Fun = true |}) ]
let app = WebApplication.Create()
app.UseRouting().UseFalco(endpoints).Run(Response.ofPlainText "Not found")
Falco's API changes between major versions, so check the version you install against its documentation.
Choosing
| Option | Good for |
|---|---|
| ASP.NET Core minimal APIs | teams mixing C# and F#, following Microsoft docs directly |
| Giraffe | the most widely used F# web library; functional handlers |
| Saturn | a batteries-included, convention-based structure |
| Falco | small, fast services with minimal abstraction |
All of them run on the same ASP.NET Core pipeline, so middleware, authentication, logging, and deployment work the same way. For full-stack F#, the SAFE Stack combines Saturn/Giraffe on the server with Fable and Elmish in the browser.