Endpoint Routing with F# / Giraffe

Date: 2023-01-11 | fsharp | giraffe | dotnet | routing |

In this post we're going to walk through web endpoint routing with F# using the Giraffe web framework. We'll go over:

  • Best practices
  • Common routing operations
  • Full source code and example project

F# and Giraffe

In this tutorial we'll be using F# and the Giraffe web framework. This is my favorite tech stack for building simple, scalable web APIs.

  • F# - Productive, functional-first language backed by the .NET ecosystem (99% of C# libs are natively callable from F#)
  • Giraffe - Simple, performant functional web framework and one of the most popular in the F# ecosystem

Prerequisites

I'll be explaining a lot as I go but there are a few topics we won't go into depth on that will be useful for you to know / have installed to get the most out of this post.

  • dotnet CLI - This will allow you to run .NET apps from your command line and thus will allow you to run the F# / Giraffe examples in this post. Download .NET from Microsoft
  • Basic understanding of network requests - Basically just need to know that most things on the internet talk to each other through "web / network requests".
  • Basic understanding of F# / Giraffe - I'll be explaining how things work within the scope of routing but everything else is out of scope. For primers on F# and Giraffe, checkout: Build a simple F# web API with Giraffe

With that out of the way, let's dive into routing.

Giraffe Routing

Routing in Giraffe is pretty simple. We essentially create a variable with a list mapping routes to the functions they should call when hit.

The general form of each map looks like this:

let endpointsList = [
    HTTPVERB [
        ROUTEFN ROUTE (HANDLER)
        // ... more routes here
    ]
    // ... more routes here
]
  • HTTPVERB - The type of request you want to handle (GET / POST / etc)
  • ROUTEFN - One of Giraffe's built-in route functions (we'll talk about these throughout the post)
  • ROUTE - The actual route string (like "/", "/posts", etc)
  • HANDLER - The function you want to use to handle calls when this endpoint is hit

Where an example of this for the simplest route would be:

(a GET endpoint on "/" that says "hello world")

let endpointsList = [
    GET [
        route "/" (text "hello world")
        // ... more routes here
    ]
    // ... more routes here
]

For the rest of this tutorial we'll be focused on just this endpointsList section of code as this is where all the routing happens.

That said, you can't just have an endpointsList variable and expect this to magically turn into a working web server. There is some configuration required to plug this into Giraffe. I'll give you an idea of how this works briefly here and then we'll move on into endpoint routing.

We can configure this endpointsList into our ASP.NET web server in our Program.fs (main entry file) like this:

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

open Giraffe
open Giraffe.EndpointRouting

(* Web App Configuration *)

let endpointsList = [
    // YOUR ENDPOINTLIST HERE!!!
]

(* Infrastructure Configuration *)

let configureApp (app : IApplicationBuilder) =
    app
        .UseRouting()
        .UseEndpoints(fun e -> e.MapGiraffeEndpoints(endpointsList))
    |> ignore

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

That should give enough context into how endpoints are configured in Giraffe for us to move forward to the endpoint routing configuration itself.

Caveat: There are currently 2 types of routing Giraffe supports - legacy routing and endpoint routing. As of Giraffe 5.x (released in 2021) "endpoint routing" is the preferred method. This is what we'll be using throughout this post.

Common Routing Operations

In this section we'll go through the most common routing operations you'll likely face when building API endpoints with F# / Giraffe.

Note: All Giraffe endpoints using endpoint routing are case-insensitive. This means that /FOO and /foo are treated the same.

HTTP Verbs

As you'd expect, all the major HTTP verbs are supported. I'll list them here for completeness but we'll just be using GET going forward for simplicity.

  • GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, TRACE, CONNECT

Full list of supported verbs via official Giraffe docs.

Basic Routes

The first thing we'll tackle is basic routes. These are just static routes that get hit when the exact string is requested from the web server (like "/" or "/my-cool-endpoint").

route

The first and most basic routing function in Giraffe is route. This just takes in a string and gets triggered when that exact string is navigated to.

So if we had something like:

let endpointsList = [
    GET [
        // Static routes
        route "/" (text "/: hello world")
        route "/my-endpoint" (text "/my-endpoint: iamacoolendpoint")
    ]
]

Then we could hit:

  • / and get a "/: hello world" back
  • /my-endpoint and get "/my-endpoint: iamanendpoint" back

subRoute

The next basic routing function is subRoute. This essentially allows you to folder the enclosed routes by prepending all routes within it with the subroute. This is very useful for separating / organizing routes by domain.

So if we had something like:

let endpointsList = [
    subRoute "/subroute/" [
        GET [
            route "one" (text "subroute/one: hit")
        ]
        subRoute "subsubroute/" [
            route "one" (text "subroute/subsubroute/one: hit")
        ]
    ]
]

We would now be able to hit:

  • /subroute/one and get back text "subroute/one: hit"
  • /subroute/subsubroute/one and get back text "subroute/subsubroute/one: hit"

You can chain subRoutes within each other to create cascading folders of routes if you wish.

Parameterized Routes

The basic routes will get you far and in most cases will make up the majority of routes you need. But sometimes you want to parameterize your routes to add some additional flexibility.

Note: When we say parameterized routes here, I'm talking about parameters in the url route itself - not url parameters attached to the end (e.g. /my-route?param=value) or sent in the body of a request. Dealing with payloads is outside the scope of this post.

routef

A common example in traditional HTTP / REST endpoints is to access a specific resource ID. So if we had a blog api that had posts, we might have an endpoint like:

  • GET /posts/POSTID -> Returns post with ID = POSTID

We can configure a route like this in Giraffe using the routef function. This allows us to register a string pattern as the url containing type-checked parameters. It behaves similarly to how a formatted string would.

So if we had something like:

let endpointsList = [
    GET [
        routef "/%s" (fun (aString : string) ->
            text ($"/aString: hit with val: {aString}"))
        routef "/%s/%i" (fun (aString : string, anInt : int) ->
            text ($"/aString/anInt: hit with vals: {aString}, {anInt}"))

    ]
]

We would now be able to hit:

  • /anyString, /willmatch, /thefirstroute -> will hit the first route with just a string param
  • /anyStringWithAnInt/0, /willmatch/1, /thesecondroute/2 -> will match the second route with both a string and int param

These parameters are type-checked so if you have an int parameter but it's not parseable as an int the request will fail.

The full list of format string identifiers to the types they support are as follows:

  • %b - bool
  • %c - char
  • %s - string
  • %i - int
  • %d - int64
  • %f - float / double
  • %O - Guid
  • %u - unit64

For more on routef usage, see routef - official Giraffe docs.

More Routing Options

The routing options provided in this post should cover the operations needed for most web apps. That said, some apps need something a bit more flexible / custom.

Giraffe is built to be a functional wrapper around ASP.NET and the routing gives us access to the underlying APIs should we need it. Doing so is outside the scope of this post but some resources to help you get started:

Next Steps

Building F# web services? Checkout CloudSeed for a production-ready F# web api boilerplate built with Giraffe. It includes routing, dependency injection, data migrations, data integrations, and more to get you started with a simple, scalable foundation for web apps.

Want more like this?

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