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 theHttpContext
- 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.