Skip to content
AgileLogics
8 min read

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()
  • choose tries each handler in turn until one matches.
  • routef "/todos/%i" parses the id as an int using a typed format, like printf.
  • >=> 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.