Build a Simple F# WebAPI with a Data Repository (F# + Giraffe)

Date: 2024-10-07 | create | tech | fsharp | giraffe | dotnet | webapp |

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.

Web API Overview

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:

  • SimpleItem - Has an Id property which is a Guid

We will build a Data Repository to hold our data and provide a central place for common operations on it.

  • Create - Create a new SimpleItem
  • GetAll - Get all SimpleItems
  • GetOne id - Get the SimpleItem with a matching Id if it exists

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 it

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

Building a Simple Data Model

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.

SimpleItemRepo

  • Initializes itself with an empty list
  • GetAll - Returns all SimpleItems
  • GetOne 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 caller

SimpleItemRepo 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

Building Endpoints with F# / Giraffe

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.

Get All Items - getAllHttpHandler

  • Gets a count of SimpleItems
  • Gets all SimpleItem ids
  • Returns these as a string

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

Get One Item - detailHttpHandler

  • Takes in an id and searches the repo for it

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!

Create Item - createHttpHandler

  • Creates a new Item for us

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 }

All Code

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()

Next

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:

Want more like this?

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