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 postsPOST /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 thedotnet
cli which is useful for accessing these functions
Interesting read on some dark patterns used to spoof techempower web framework benchmarks: https://t.co/8iDsMr4TRO
— Hamilton Greene (@SIRHAMY) November 26, 2022
If we look at just "Full" benchmarks we find #fsharp #giraffe in a respectable 21st - https://t.co/RagNLcyF3a pic.twitter.com/3t0NuiPECk
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.
- Clone the repo: fsharp-giraffe-blog-api-example
- To run:
dotnet run
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
endpointPOST /posts/create
- Creates a new blog post with the payload- Payload (json):
- title: string
- content: string
- Payload (json):
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 appBlog.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 dotnetCompile 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 listGiraffe
as a dependency - this is done by thedotnet 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 likeusing
in c# orimport
in jsWeb 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 ofiamanapi
GET /posts
will call intogetPostsHttpHandler
and returnPOST /posts/create
will call intocreatePostHttpHandler
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
- Routes:
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
andcontent
field that are strings. We use this inBlogDb
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).
- BlogPost - Domain Model - On one hand it's representing what a BlogPost means in our application. Just a
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 utilizeOOP
patterns in F# when usefulHttpHandler
- 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 ourServiceTree
(for access to the shared BlogDb) and returns anHttpHandler
which will be called by the Giraffe request pipeline on request and return the contents of BlogDbcreatePostHttpHandler
- Similar to above, but also utilizes theHttpContext
passed in from Giraffe to fetch the payload associated with the request - in this case we're looking for a JSON payload ofBlogPost
. 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:
- Run F# / .NET in Docker
- CloudSeed - A production-ready F# API boilerplate built on Giraffe -> see Up and Running with CloudSeed (F# / SvelteKit Boilerplate)
Want more like this?
The best / easiest way to support my work is by subscribing for future updates and sharing with your network.