Essay - Published: 2024.10.07 | create | dotnet | fsharp | giraffe | tech | webapp |
DISCLOSURE: If you buy through affiliate links, I may earn a small commission. (disclosures)
Previously we walked through how to create a minimal single-file webapi with F# / Giraffe. This gets you off the ground with a simple web api but it doesn't offer a great example for how you might actually build a web app with this - data and all.
So in this post we'll be expanding on that minimal F# / Giraffe web api to build common endpoints and data operations.
In this post we'll be focusing on data and endpoints. If you want more info about F# / Giraffe itself (including configuration) then checkout my previous post.
This web API will have a very simple data model:
We will build a Data Repository to hold our data and provide a central place for common operations on it.
This is overly simplistic but should give you an idea for how you might start a webapp and expand to your own data models, more advanced functionality.
To access this data we'll have 3 web API endpoints:
/ - Index showing a count of SimpleItems and all SimpleItem Ids/detail/id - Searches for a SimpleItem with matching Id/create - Creates a new SimpleItem and returns itAgain very simplistic but provides an example of how you might build an API and link it to data operations.
All project files (github) are available to HAMINIONs members.
Okay now let's build the data model and repository.
First we declare a SimpleItem which is the data our webapp will operate on. It's just a record with an Id property.
type SimpleItem =
{
Id: Guid
}
Next we create a Data Repository. This is a common pattern for handling data in software systems - it helps to isolate and centralize data access.
GetAll - Returns all SimpleItemsGetOne id - Searches for an item with matching id. Some if found, None if not.Create - Creates a new item, saves it, and returns it to the callerSimpleItemRepo code
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
Now let's look at our endpoints and how they interact with our repo to access our data.
First I'll show you how we declare our endpoints so Giraffe knows what to do when it receives a web request to the target url.
/ - Gets all SimpleItems/detail/id - Gets SimpleItem with matching id (or says Not Found)/create - Creates a new item (and returns it)Endpoints code:
let webApp =
[
GET [
route "/" (getAllHttpHandler)
routef "/detail/%s" (
fun id -> detailHttpHandler id
)
route "/create" (createHttpHandler)
]
]
(for how we hook up these endpoints to F# / Giraffe see the full source code or checkout my previous post - Build a Simple Single-File Web API with F# / Giraffe)
Now we'll go through each of these HttpHandlers one by one to give you an idea of how they work.
Code:
let getAllHttpHandler =
handleContext( 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! ctx.WriteTextAsync (indexPageString)
}
)
Example Output:
ItemCount: 5
b2241dc0-49e8-4a01-b330-6319bfd97165
485bbda7-8f3a-4b22-ad04-e2e77ab00c33
c0f15253-0ab7-4786-8ba0-52844e139d69
75e97fbc-15ca-4872-a332-1f0eb3bb8f24
e7fc75b4-7a5d-4c80-bc74-292f8ef32826
Code:
let detailHttpHandler
(id: string)
=
handleContext( fun (ctx: HttpContext) ->
task {
let item = itemRepo.GetOne(id)
return! ctx.WriteTextAsync
(match item with
| Some s -> string item
| None -> "Not found!")
}
)
Example Output - Item Found - /detail/b2241dc0-49e8-4a01-b330-6319bfd97165
Some({ Id = b2241dc0-49e8-4a01-b330-6319bfd97165 })
Example Output - Not Found - /detail/idontexist
Not found!
Code:
let createHttpHandler =
handleContext( fun (ctx: HttpContext) ->
task {
let newItem = itemRepo.Create()
return! ctx.WriteTextAsync (string newItem)
}
)
Example Output:
{ Id = b2241dc0-49e8-4a01-b330-6319bfd97165 }
For posterity - here's the full Program.fs file running this program.
Want the full project source code including all project files? HAMINIONS Members get access to this project's full source code (github) along with dozens of other example projects.
Program.fs
open System
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.Http
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Hosting
open Microsoft.Extensions.Logging
open Giraffe
open Giraffe.EndpointRouting
(*
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 =
handleContext( 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! ctx.WriteTextAsync (indexPageString)
}
)
let detailHttpHandler
(id: string)
=
handleContext( fun (ctx: HttpContext) ->
task {
let item = itemRepo.GetOne(id)
return! ctx.WriteTextAsync
(match item with
| Some s -> string item
| None -> "Not found!")
}
)
let createHttpHandler =
handleContext( fun (ctx: HttpContext) ->
task {
let newItem = itemRepo.Create()
return! ctx.WriteTextAsync (string newItem)
}
)
let webApp =
[
GET [
route "/" (getAllHttpHandler)
routef "/detail/%s" (
fun id -> detailHttpHandler id
)
route "/create" (createHttpHandler)
]
]
// ---------------------------------
// Config and Main
// ---------------------------------
let configureApp (app: IApplicationBuilder) =
app
.UseRouting()
.UseEndpoints(fun e -> e.MapGiraffeEndpoints(webApp))
|> ignore
let configureServices (services: IServiceCollection) =
services.AddRouting() |> ignore
// Add Giraffe dependencies
services.AddGiraffe() |> ignore
Host
.CreateDefaultBuilder()
.ConfigureWebHost(fun webHost ->
webHost
.UseKestrel(fun c -> c.AddServerHeader <- false)
.ConfigureServices(configureServices)
.Configure(configureApp)
|> ignore)
.Build()
.Run()
There you have it - a simple web API built with F# / Giraffe that includes data operations using a Data Repository pattern. Hopefully this gives you a better idea of how you might build a web app that needs to process data (most of them).
Want to build production-ready webapps with F#? I use CloudSeed to start all my F# web projects - it helps me get up and running with a solid base in 10 minutes.
If you liked this post you might also like:
The best way to support my work is to like / comment / share for the algorithm and subscribe for future updates.