Build a Single-File Web API with F# + Falco

Date: 2024-10-30 | create | tech | fsharp | falco | webapp |

I've primarily been building webapps with F# + Giraffe for the past few years as it's stable, performs well, and has a reasonable ecosystem / API. It works well so I haven't had much of a reason to switch to a different framework.

But Giraffe is not the only production-ready web framework available in F# land and a lot of work has gone into evolving them since I last took a look.

In this post we'll be diving into one of these frameworks - Falco (version 5) - and walk through creating a single-file, minimal web api.

Web API Overview

This web API will have 3 endpoints:

  • / - Lists all items
  • /create - Creates a new item
  • /detail/ID - Gets the item with the specified ID (or says Not Found)

Note: This is the same API we used previously in our F# + Giraffe Web API example so hopefully it's relatively easy to compare and contrast between the frameworks.

For data persistence we're using an in-memory repo - basically a class that wraps an array of items and surfaces some operations like Create, GetOne, and GetAll.

Data Repo code:

type SimpleItem = 
    {
        Id: Guid
    }

let stringToGuidOption (s: string) : Guid option =
    match Guid.TryParse(s) with
    | (true, guid) -> Some guid
    | (false, _) -> None

type SimpleItemRepo() =

    let mutable allItems = []

    member this.GetAll() = allItems 

    member this.GetOne (id: string) : SimpleItem option = 
        
        stringToGuidOption id 
        |> Option.bind ( fun id -> 
            allItems
            |> List.tryFind (fun i -> i.Id = id)
        )

    member this.Create() = 
        let id = Guid.NewGuid()
        let newItem = { Id = id }

        allItems <- newItem :: allItems

        newItem

For more on how this Data Repo works, checkout Build a Simple F# WebAPI with a Data Repository (F# + Giraffe)

The full project source code (github) is available to HAMINIONs members so you can clone and run this example project on your own machine.

Falco Web API

Falco's endpoints use much the same interface as Giraffe's - they're built around the idea of an HttpHandler, here defined as HttpContext -> Task (github). The HttpContext gives you access to the details of a request like route parameters, headers, and injected services from ASP Dotnet.

First let's setup our endpoint mappings - here a list of urls to their HttpHandlers:

let webAppEndpoints =
    [
        get "/" getAllHttpHandler 
        get "/detail/{id}" (detailHttpHandler)
        get "/create" createHttpHandler
    ]

Then we can define our endpoint handlers:

getAllHttpHandler

  • Gets the count of items in our data repo
  • Gets the ids of all items in our repo
  • Constructs a string to send back to requester
  • Returns a PlainText string using Response.ofPlainText
let getAllHttpHandler: HttpHandler =
    fun (ctx: HttpContext) -> 
        task {
            let count = itemRepo.GetAll().Length

            let allItemsStringRepresentation = 
                itemRepo.GetAll()
                |> List.map (fun i -> i.Id.ToString())
                |> String.concat "\n"

            let indexPageString = $"ItemCount: {count} \n {allItemsStringRepresentation}"

            return 
                Response.ofPlainText
                    indexPageString
                    ctx
        }

detailHttpHandler

Recall that our detail route has form "/detail/{id}", this allows us to look for id in our route to get the value out.

  • Gets the id string from our route using the HttpContext
  • Tries to get the matching Item from our Data Repo
  • Returns a string depending on if the item was found or not
let detailHttpHandler: HttpHandler =
    fun (ctx: HttpContext) -> 
        task {
            let route = Request.getRoute ctx 
            let idParam = route.GetString "id"

            let item = itemRepo.GetOne(idParam)

            return 
                Response.ofPlainText
                    (match item with 
                    | Some s -> string item 
                    | None -> "Not found!")
                ctx
        }

createHttpHandler

  • Creates a new Item in the Data Repo
  • Returns a string representation of the new item
let createHttpHandler: HttpHandler =
    fun (ctx: HttpContext) -> 
        task {

            let newItem = itemRepo.Create()

            return 
                Response.ofPlainText
                    (string newItem)
                    ctx
        }

Hooking up Falco to ASP Dotnet

We now have the logic of our web api - endpoints, handlers, and data repo - but we still need to hook it into ASP Dotnet so it actually spins up when we run our app.

Here we use a similar configuration to what we used for our F# + Giraffe Single-File Web API for ease of comparing / contrasting. That said it seems like there are probably simpler configurations you can use according to Falco's docs so while this config works, it may not be optimal.

let configureApp (app: IApplicationBuilder) =
    app
        .UseRouting()
        .UseFalco(webAppEndpoints)
    |> ignore

let configureServices (services: IServiceCollection) = 
    services.AddRouting() |> ignore

Host
    .CreateDefaultBuilder()
    .ConfigureWebHost(fun webHost ->
        webHost
            .UseKestrel(fun c -> c.AddServerHeader <- false)
            .ConfigureServices(configureServices)
            .Configure(configureApp)
        |> ignore)
    .Build()
    .Run()

Full Single-File Web API Code

Here's the full code so you can copy/paste it.

open System

open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.Http
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Hosting

open Falco
open Falco.Routing

(*
    Data and Services
*)

type SimpleItem = 
    {
        Id: Guid
    }

let stringToGuidOption (s: string) : Guid option =
    match Guid.TryParse(s) with
    | (true, guid) -> Some guid
    | (false, _) -> None

type SimpleItemRepo() =

    let mutable allItems = []

    member this.GetAll() = allItems 

    member this.GetOne (id: string) : SimpleItem option = 
        
        stringToGuidOption id 
        |> Option.bind ( fun id -> 
            allItems
            |> List.tryFind (fun i -> i.Id = id)
        )

    member this.Create() = 
        let id = Guid.NewGuid()
        let newItem = { Id = id }

        allItems <- newItem :: allItems

        newItem

// ---------------------------------
// Web app
// ---------------------------------

let itemRepo = SimpleItemRepo()

let getAllHttpHandler: HttpHandler =
    fun (ctx: HttpContext) -> 
        task {
            let count = itemRepo.GetAll().Length

            let allItemsStringRepresentation = 
                itemRepo.GetAll()
                |> List.map (fun i -> i.Id.ToString())
                |> String.concat "\n"

            let indexPageString = $"ItemCount: {count} \n {allItemsStringRepresentation}"

            return 
                Response.ofPlainText
                    indexPageString
                    ctx
        }

let detailHttpHandler: HttpHandler =
    fun (ctx: HttpContext) -> 
        task {
            let route = Request.getRoute ctx 
            let idParam = route.GetString "id"

            let item = itemRepo.GetOne(idParam)

            return 
                Response.ofPlainText
                    (match item with 
                    | Some s -> string item 
                    | None -> "Not found!")
                ctx
        }

let createHttpHandler: HttpHandler =
    fun (ctx: HttpContext) -> 
        task {

            let newItem = itemRepo.Create()

            return 
                Response.ofPlainText
                    (string newItem)
                    ctx
        }

let webAppEndpoints =
    [
        get "/" getAllHttpHandler 
        get "/detail/{id}" (detailHttpHandler)
        get "/create" createHttpHandler
    ]

// ---------------------------------
// Config and Main
// ---------------------------------

let configureApp (app: IApplicationBuilder) =
    app
        .UseRouting()
        .UseFalco(webAppEndpoints)
    |> ignore

let configureServices (services: IServiceCollection) = 
    services.AddRouting() |> ignore

Host
    .CreateDefaultBuilder()
    .ConfigureWebHost(fun webHost ->
        webHost
            .UseKestrel(fun c -> c.AddServerHeader <- false)
            .ConfigureServices(configureServices)
            .Configure(configureApp)
        |> ignore)
    .Build()
    .Run()

Next

Overall I really like Falco - it's lean with minimal boilerplate to get your webapp up and running. I'm really happy we have several good web frameworks in the ecosystem - it gives people options and helps push F# forward.

I've just started playing around with Falco so am still exploring capabilities and best techniques. Let me know if there's something you'd like me to explore / cover and I'll work my way over there. Top of mind is doing Server-side rendered HTML with Falco's HTML DSL and maybe examples building full apps with it.

Thank you HAMINIONs for your support! As a reminder - HAMINIONs Members get access to this example's full source code (github) so you can clone and run it on your own machine. It also includes access to dozens of other examples covered in my guides (plus you support me creating more posts like this one!).

If you liked this post you might also like:

Want more like this?

The best / easiest way to support my work is by subscribing for future updates and sharing with your network.