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#.
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:
- Return a response, by returning
Some ctx
(wherectx
is aHttpContext
) - Pass the request on to the next handler in the pipeline by calling the “next” function
- 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:
- The next function in the pipeline
- A
HttpContext
object that represents the current request context
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!"
]
- The
choose
function tries to send the current request to a list of handlers; if a handler returnsNone
it will try the next handler in the list, and so on. route "/"
is a handler that checks the path of the current request; if it’s equal to"/"
, then it will pass the request on to the next handler. If the path is not equal to"/"
, it will returnNone
.text
simply returns the given string as a response.
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:
- An F# format string
- 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!