Building REST APIs in Giraffe: Introduction

2024-12-16

Giraffe is a powerful F# framework for creating web applications, built on top of ASP.NET Core. In this series of blog posts, you will learn how to use F# and Giraffe to write a simple REST API for managing to-do lists.

To follow this tutorial you will need some familiarity with F#. If you want to get to know the basics before continuing, you can check out the course Take your first steps with F#.

A giraffe with blue sky in the background

Setup

First of all, ensure that you have an up-to-date version of .NET installed. Once that’s out of the way, let’s create a new F# application. We will use the web template, which gives us an empty ASP.NET Core project:

dotnet new web -lang F# -o Todo.API

Next, move to the newly created project directory and install Giraffe:

cd Todo.API
dotnet add package Giraffe

Your first Giraffe application

With everything installed, let’s take Giraffe for a spin! The dotnet new command should have created a Program.fs file for us, which is the entry point of our application. Open up Program.fs in your favourite editor and replace its contents with the following:

open Giraffe

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

let webApp =
    choose [
        route "/" >=> text "Hello, world!"
    ]

let configureApp (app : IApplicationBuilder) =
    // Add Giraffe to the ASP.NET Core pipeline
    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

You don’t need to know what all of the lines do—most of it is just boilerplate to start ASP.NET and set up Giraffe. The important parts are these lines:

let webApp =
    choose [
        route "/" >=> text "Hello, world!"
    ]

This is the core of our Giraffe application, and we will walk through it in detail below.

Just to check that everything is working, call dotnet run from your shell; you should now be able to open the URL http://127.0.0.1:5009 in your browser and be greeted with “Hello, world!”. Cool!

Handlers

You can think of a Giraffe application as a pipeline of functions. Each function in the pipeline is called a “handler”. Handlers are connected with the >=> operator.

When you send a Giraffe a request, it passes the request down the handler pipeline. Each handler in a pipeline may do one of three things:

  1. Return a response, by returning Some ctx (where ctx is a HttpContext)
  2. Pass the request on to the next handler in the pipeline by calling the “next” function
  3. Return None

If the function returns None, it means that the handler couldn’t process the request, and the application should move on to another part of the pipeline.

Defining handlers

A Giraffe handler function accepts two arguments:

Since Giraffe is an asynchronous framework, all handler functions return Tasks. The value of the completed task should be a HttpContext option, which means each handler is expected to return a Task<HttpContext option>.

Now that we know how handlers work, let’s break down the Giraffe app we defined above:

let webApp =
    choose [
        route "/" >=> text "Hello, world!"
    ]

Writing responses

The request context contains information about the current request, but it also lets us do other things related to requests and responses, such as writing text to the response body. It also lets us access ASP.NET Core functionality, such as dependency injection, authentication and so on.

In the previous example we used Giraffe’s built-in text handler to output a response, but if we wanted to write our own “Hello, world!” handler we could have done it like this:

let helloWorld next (ctx: HttpContext) =
    ctx.WriteTextAsync "Hello, world!"

We can then connect it to the "/" route like so:

let webApp =
    choose [
        route "/" >=> helloWorld
    ]

Higher-order handlers

To make handlers composable, it’s quite common to write functions that return handler functions. For example, we could write our own version of Giraffe’s text function like this:

let text body =
    fun next (ctx: HttpContext) ->
        ctx.WriteTextAsync body

If we call this function with the argument "Hello, world!" it will return a handler function that writes “Hello, world!” to the response body.

Since F# supports function currying we could also have written the text function like this:

let text body next (ctx: HttpContext) =
    ctx.WriteTextAsync body

As an exercise, let’s write a simplified version of Giraffe’s built-in route handler: As we saw previously, route should take a “path” string as its first argument. If the HTTP request path matches the “path” string, we should continue to the next handler in the pipeline, otherwise we should abort. Here’s how we might implement it:

let route' path next (ctx: HttpContext) = task {
    if ctx.Request.Path = path then
        return! next ctx
    else
        return None
}

We first get the current request path from ctx.Request.Path and compare that with the path argument. If they are equal we call next ctx, which means we move on to the next handler; if they are not equal we return None.

Note that we wrap the entire handler in a task expression, as this is a convenient way to make the handler return a Task. Also, note that we use return! when we call next, as the next function will also return a Task; had we used the regular return we would end up with a task nested inside a task, i.e. Task<Task<HttpContext option>>.

Dynamic routing

Sometimes you want routes to be dynamic, i.e. the route should change based on what it points to. For example, given that you have a “user” in your app with ID 123, you might access that user at a path like /users/123. In Giraffe we can create dynamic routes with the routef function.

The routef function accepts two arguments:

  1. An F# format string
  2. A function that should return a handler.

The format string can contain placeholders characters that make up the dynamic parts of the URL. For example, the placeholder %s will match any string, except certain special characters, like "/". When the route is looked up, the formatting string is matched against the URL path, and the placeholder parts are captured and passed to the function as arguments.

Here’s an example of a more dynamic “Hello, world!”:

routef "/greetings/%s"
    (fun location -> text $"Hello, %s{location}!")

If the user makes a request to /greetings/Helsinki, it will match the "/greetings/%s" route; "Helsinki" will then be captured and passed as an argument to the hello function, which returns Hello, Helsinki!.

Note that the type of the location argument must match the type of the placeholder argument in the formatting string! Had we used the %i placeholder then location would have to be an integer, for example. For a complete list of placeholder characters and their types, see the routef documentation.

We can also use several placeholder characters in the same format string:

let helloCity (state, city) next (ctx: HttpContext) =
    ctx.WriteTextAsync $"Hello, %s{city}, %s{state}!"

let webApp =
    choose [
        routef "/greetings/%s/%s" helloCity
    ]

When we use multiple placeholder characters, the function will receive a tuple, with one element for each placeholder. In the example above, we used two string placeholders (%s) and so the helloCity function will receive a tuple of two string arguments.

Next time

That’s it for this instalment! Next time we will start to implement our to-do application, which means we’ll add a bunch more routes and accept user input. To be sure you don’t miss it when it comes out, follow Functional Sofware on LinkedIn!