Build a simple F# web API with Giraffe

Date: 2022-12-07 | create | tech | fsharp | api | giraffe | dotnet |

This post is part of the 2022 F# Advent Calendar.

In this post, we'll walk through how to build a simple F# web API with Giraffe - a functional micro framework on ASP.NET Core.

Web API Overview

For this tutorial, we'll build a sample API to power a simple blog. It will only have two endpoints:

  • GET /posts - Returns all blog posts
  • POST /posts/create - Creates a new blog post with the payload

We should be able to touch all the core building blocks you need to create your own API.

Requirements

In order to successfully build and run this API, you'll need:

  • .NET SDK - This gives us access to the libraries we need to build and run .NET apps (F# runs on .NET) and it gives us access to the dotnet cli which is useful for accessing these functions

Web API Library

For this tutorial we'll be using Giraffe. There are other good F# web frameworks out there, but I think Giraffe is the best for most people and is officially endorsed by F#.

Some things going for it:

  • Light-weight - Requires minimal boilerplate (as we'll see) while providing all the composability of functional paradigms and capabilities of ASP.NET
  • Well supported - Giraffe has been popular in the community for some time receiving regular updates and tutorials (like this one)
  • Performant - Giraffe regularly places high in benchmarks like Tech Empower's Web Framework Benchmarks and piggybacks off all the improvements being pushed into ASP.NET Core

To install this, you can simply run:

dotnet add package giraffe

For more info, you can check out Giraffe's official documentation.

Running the Web API

We'll start this tutorial by first showing you what this API does. We'll do this by running the code and follow that with explaining how it works.

Once you've successfully cloned the repo and run dotnet, you should get an output telling you where the application is listening. Something like this (your ports will likely be different):

info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /root/2022/Projects/fsharp-giraffe-blog-api-example

This tells us that the web app is now serving and can be reached from two urls - ports assigned to our local machine:

  • http - localhost:5000
  • https - localhost:5001

Note: these ports may be different on your machine so use whatever your machine tells you.

We can now hit our endpoints and see it works as expected:

  • GET /posts - Returns all blog posts created via the /posts/create endpoint
  • POST /posts/create - Creates a new blog post with the payload
    • Payload (json):
      • title: string
      • content: string

Code Walkthrough

Now that we know how the api behaves, let's dive into the code that allows it to function. We'll walk through each individual part and how it goes together here, but you can also follow along with the full source code on GitHub.

The three parts we have are:

  • Project file - Metadata on how to build our project (like a package.json)
  • Program.fs - The "main" entrypoing of our web app
  • Blog.fs - A few utilities to enable our blog functionality

Project file

In the repo, you'll see a *.fsproj file. This is an F# project file that holds metadata about the project like which files need to be compiled in which order and what external libraries need to be used to run.

*.fsproj

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <RootNamespace>_2022_12_fsharp_giraffe_example</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="Blog.fs" />
    <Compile Include="Program.fs" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Giraffe" Version="6.0.0" />
  </ItemGroup>

</Project>

This is pretty straight forward and familiar if you've built projects in other languages before:

  • TargetFramework - lists our runtime target as .NET 6, the current LTS version of dotnet
  • Compile Include= - These declare that the project should compile these F# files - and do so in this order. F# does not allow circular dependencies and requires that compilation order is followed - top to bottom. We'll see this reflected in the resulting code.
  • PackageReference - Finally we list Giraffe as a dependency - this is done by the dotnet add package command

Program.fs

Program.fs is our application root where we actually spin up our webserver and connect http routes to our application code.

Program.fs

open System
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.Extensions.Hosting
open Microsoft.Extensions.Logging
open Microsoft.Extensions.DependencyInjection

open Blog
open Giraffe

(* Web App Configuration *)

let webApp = 
    let blogDb = new BlogDb()

    let serviceTree = {
        getBlogDb = fun() -> blogDb
    }

    choose[
        route "/" >=> text "iamanapi"
        subRoute "/posts" 
            (choose [
                route "" >=> GET >=> warbler (fun _ -> 
                    (getPostsHttpHandler serviceTree))
                route "/create" 
                    >=> POST 
                    >=> warbler (fun _ -> 
                        (createPostHttpHandler serviceTree))
            ])
    ]

(* Infrastructure Configuration *)

let configureApp (app : IApplicationBuilder) =
    app.UseGiraffe (webApp)

let configureServices (services : IServiceCollection) =
    // Add Giraffe dependencies
    services.AddGiraffe() |> ignore

[<EntryPoint>]
let main _ =
    Host.CreateDefaultBuilder()
        .ConfigureWebHostDefaults(
            fun webHostBuilder ->
                webHostBuilder
                    .Configure(configureApp)
                    .ConfigureServices(configureServices)
                    |> ignore)
        .Build()
        .Run()
    0
  • open dependencies - open in F# is like using in c# or import in js
  • Web App Configuration - In this section we're actually declaring what API routes we want to expose and how they call into our code.
    • Routes:
      • / returns a string of iamanapi
      • GET /posts will call into getPostsHttpHandler and return
      • POST /posts/create will call into createPostHttpHandler
      • warbler is a special function that says a route's return type is not static (i.e. it will change). This forces the route to be reevaluated each time it's called vs returning the same value. Warbler docs
    • serviceTree - This is a very simple method of manual dependency injection at the composition root - it's how we create a piece of shared state across our requests. These terms are out of scope for this post but you should be able to find more by searching those terms. For an example of this paradigm at scale, checkout the CloudSeed F# Boilerplate
  • Infrastructure Configuration - For the most part you can just think of this as boilerplate. This is how we plug Giraffe into the underlying ASP.NET webserver. There are a lot of potential configuration items here but they're out ofscope

Blog.fs

Finally we'll dive into the specific code that gives our API basic blog functionality.

Blog.fs

module Blog 

open System
open System.Threading.Tasks
open Microsoft.AspNetCore.Http
open Giraffe

[<CLIMutable>]
type BlogPost = {
    title: string
    content: string
}

type BlogDb() =

    let mutable allBlogPosts : BlogPost list = []

    member this.GetAllPosts = fun() -> allBlogPosts 

    member this.AddPost (newPost : BlogPost) =
        allBlogPosts <- (newPost::allBlogPosts)
        allBlogPosts

type BlogServiceTree = {
    getBlogDb : unit -> BlogDb
}

let getPostsHttpHandler (serviceTree: BlogServiceTree) =
    fun (next : HttpFunc) (ctx : HttpContext) ->
        json (serviceTree.getBlogDb().GetAllPosts()) next ctx

let createPostHttpHandler (serviceTree: BlogServiceTree) =
    fun (next : HttpFunc) (ctx : HttpContext) ->
        task {
            let! newPostJson = ctx.BindJsonAsync<BlogPost>()
            serviceTree.getBlogDb().AddPost(newPostJson) |> ignore
            return! json (newPostJson) next ctx
        }
  • BlogPost - This type is doing double duty for us. This is maybe not ideal real-world, but is useful to keep things simple here.
    • BlogPost - Domain Model - On one hand it's representing what a BlogPost means in our application. Just a title and content field that are strings. We use this in BlogDb to actually store our data across requests.
    • BlogPost - DTO - However we also use this within createPostHttpHandler to define the data shape we expect from the http request that hits this. Because we use it in this way, we need the [<CLIMutable>] decorator to basically allow the framework to break some default rules allowing us to dynamically create this type from unknown inputs (the request).
  • BlogDb - Here we create a very simple type to store our shared state. We probably wouldn't use this in production, but it shows how simple it can be to utilize OOP patterns in F# when useful
  • HttpHandler - This is our connector from the web world of Giraffe (Program.fs) and our domain code (Blog.fs). Giraffe has a particular way of dealing with this called HttpHandlers (Giraffe HttpHandler Documentation)
    • getPostsHttpHandler - Takes in our ServiceTree (for access to the shared BlogDb) and returns an HttpHandler which will be called by the Giraffe request pipeline on request and return the contents of BlogDb
    • createPostHttpHandler - Similar to above, but also utilizes the HttpContext passed in from Giraffe to fetch the payload associated with the request - in this case we're looking for a JSON payload of BlogPost. It then saves it in BlogDb and returns it in json form.

Next Steps

There's a lot of code here to reason about but I'd highly recommend just cloning the GitHub repo and trying it out yourself. I think you'll find that building web apis with F# + Giraffe is quite ergonomic once you get going.

You might also be interested in:

Want more like this?

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