Essay - Published: 2022.12.07 | api | create | dotnet | featured | fsharp | giraffe | tech |
DISCLOSURE: If you buy through affiliate links, I may earn a small commission. (disclosures)
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.
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 payloadWe should be able to touch all the core building blocks you need to create your own API.
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 functionsInteresting 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
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:
To install this, you can simply run:
dotnet add package giraffe
For more info, you can check out Giraffe's official documentation.
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.
dotnet runOnce 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:
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
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 functionalityIn 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 list Giraffe as a dependency - this is done by the dotnet add package commandProgram.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 jsWeb App Configuration - In this section we're actually declaring what API routes we want to expose and how they call into our code.
/ returns a string of iamanapiGET /posts will call into getPostsHttpHandler and returnPOST /posts/create will call into createPostHttpHandlerwarbler 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 docsserviceTree - 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# BoilerplateInfrastructure 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 ofscopeFinally 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.
title and content field that are strings. We use this in BlogDb to actually store our data across requests.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 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 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 BlogDbcreatePostHttpHandler - 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.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:
The best way to support my work is to like / comment / share for the algorithm and subscribe for future updates.