Giraffe

Guide to building functional REST APIs with Giraffe framework for F#

Introduction to Giraffe

Giraffe is a functional ASP.NET Core middleware library for building web applications. It provides a functional programming model that integrates seamlessly with the ASP.NET Core pipeline while leveraging F#’s powerful type system and concise syntax.

Framework Overview

Giraffe combines the performance and ecosystem benefits of ASP.NET Core with the elegance and safety of functional programming:

  • Functional first: Designed specifically for F#, emphasizing immutability and function composition
  • Middleware-based: Works as middleware in the ASP.NET Core pipeline
  • High performance: Maintains the speed benefits of ASP.NET Core
  • Type-safe routing: Uses F#’s type system to create safer route handlers
  • Lightweight: Minimal dependencies and focused on core HTTP functionality

When to Choose Giraffe

Giraffe is an excellent choice when:

  • You prefer functional programming paradigms
  • You want to use F# as your primary language
  • You need the performance and ecosystem of ASP.NET Core
  • You value composable, testable HTTP handlers
  • You want to reduce runtime errors through stronger type safety

Getting Started

Installation

To get started with Giraffe, create a new F# web project and add the Giraffe NuGet package:

dotnet new web -lang F# -o GiraffeApi
cd GiraffeApi
dotnet add package Giraffe

Basic Project Structure

Here’s a minimal Giraffe application structure:

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

// Define HTTP handlers
let webApp =
    choose [
        route "/" >=> text "Hello World from Giraffe!"
        route "/api/ping" >=> json {| message = "pong"; timestamp = DateTime.UtcNow |}
    ]

// Configure and run app
[<EntryPoint>]
let main args =
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(
            fun webHostBuilder ->
                webHostBuilder
                    .Configure(fun app -> app.UseGiraffe webApp)
                    .ConfigureServices(fun services ->
                        services.AddGiraffe() |> ignore)
                    |> ignore)
        .Build()
        .Run()
    0

Building REST APIs with Giraffe

HTTP Handlers

In Giraffe, HTTP handlers are the fundamental building blocks, similar to controller actions in traditional MVC frameworks:

// Basic HTTP handlers
let helloHandler = text "Hello, World!"
let jsonHandler = json {| message = "This is JSON" |}
let notFoundHandler = setStatusCode 404 >=> text "Not Found"

Routing

Giraffe provides a flexible routing system based on function composition:

let webApp =
    choose [
        route "/api/hello" >=> helloHandler
        routef "/api/users/%s" (fun username -> text (sprintf "Hello, %s!" username))
        routef "/api/orders/%i" (fun orderId -> json {| id = orderId; status = "processing" |})
        route "/api/products" >=> GET >=> jsonHandler
        subRoute "/api/admin" (
            requiresAuthentication (challenge "Bearer") >=>
            choose [
                route "/stats" >=> adminStatsHandler
                route "/users" >=> adminUsersHandler
            ]
        )
    ]

CRUD Operations Example

Here’s a comprehensive example of implementing RESTful CRUD operations with Giraffe:

// Model
type Product = {
    Id: int
    Name: string
    Price: decimal
    Stock: int
}

// In-memory "database"
let mutable products = [
    { Id = 1; Name = "Laptop"; Price = 1200.0m; Stock = 10 }
    { Id = 2; Name = "Mouse"; Price = 25.0m; Stock = 50 }
]

// Handlers
let getProducts = json products

let getProductById (id: int) =
    match products |> List.tryFind (fun p -> p.Id = id) with
    | Some product -> json product
    | None -> setStatusCode 404 >=> json {| error = "Product not found" |}

let createProduct : HttpHandler =
    fun (next : HttpFunc) (ctx : Microsoft.AspNetCore.Http.HttpContext) ->
        task {
            let! product = ctx.BindJsonAsync<Product>()
            products <- products @ [product]
            return! json product next ctx
        }

let updateProduct (id: int) : HttpHandler =
    fun (next : HttpFunc) (ctx : Microsoft.AspNetCore.Http.HttpContext) ->
        task {
            let! updatedProduct = ctx.BindJsonAsync<Product>()
            match products |> List.tryFindIndex (fun p -> p.Id = id) with
            | Some index ->
                products <- products |> List.mapi (fun i p -> if i = index then updatedProduct else p)
                return! json updatedProduct next ctx
            | None -> 
                return! (setStatusCode 404 >=> json {| error = "Product not found" |}) next ctx
        }

let deleteProduct (id: int) =
    let found = products |> List.exists (fun p -> p.Id = id)
    if found then
        products <- products |> List.filter (fun p -> p.Id <> id)
        setStatusCode 204 >=> text ""
    else
        setStatusCode 404 >=> json {| error = "Product not found" |}

// Routes
let productRoutes =
    choose [
        GET >=> choose [
            route "" >=> getProducts
            routef "/%i" getProductById
        ]
        POST >=> route "" >=> createProduct
        PUT >=> routef "/%i" updateProduct
        DELETE >=> routef "/%i" deleteProduct
    ]

let webApp =
    choose [
        subRoute "/api/products" productRoutes
        setStatusCode 404 >=> text "Not found"
    ]

Advanced Features

Content Negotiation

Giraffe supports content negotiation through the negotiate handler:

let handler =
    negotiate {
        json {| name = "F# Developer"; language = "F#" |}
        xml <person><name>F# Developer</name><language>F#</language></person>
    }

Dependency Injection

Giraffe integrates with ASP.NET Core’s dependency injection system:

// Service registration
let configureServices (services: IServiceCollection) =
    services.AddSingleton<IProductRepository, ProductRepository>() |> ignore
    services.AddGiraffe() |> ignore

// Using services in handlers
let getProductsHandler : HttpHandler =
    fun (next : HttpFunc) (ctx : Microsoft.AspNetCore.Http.HttpContext) ->
        let repository = ctx.GetService<IProductRepository>()
        let products = repository.GetAll()
        json products next ctx

Error Handling

Implement centralized error handling with Giraffe’s error handler middleware:

let errorHandler (ex : Exception) (logger : ILogger) =
    match ex with
    | :? ArgumentException as aex ->
        clearResponse >=> setStatusCode 400 >=> json {| error = aex.Message |}
    | :? UnauthorizedAccessException ->
        clearResponse >=> setStatusCode 401 >=> json {| error = "Unauthorized access" |}
    | _ ->
        logger.LogError(ex, "An unhandled exception occurred")
        clearResponse >=> setStatusCode 500 >=> json {| error = "Internal server error" |}

// Register the error handler
let configureApp (app : IApplicationBuilder) =
    app.UseGiraffeErrorHandler(errorHandler)
       .UseGiraffe webApp

Authentication and Authorization

Secure your API with built-in authentication middleware:

let authorize : HttpHandler =
    requiresAuthentication (challenge "Bearer") >=>
    requiresRole "Admin"

let securedHandler = 
    authorize >=> json {| message = "This is secured data"; clearance = "Top Secret" |}

let webApp =
    choose [
        route "/api/public" >=> json {| message = "This is public data" |}
        route "/api/secured" >=> securedHandler
    ]

Testing Giraffe Applications

Giraffe’s functional design makes it especially suitable for unit testing:

open NUnit.Framework
open Giraffe
open Microsoft.AspNetCore.Http
open FSharp.Control.Tasks.V2.ContextInsensitive

[<Test>]
let ``GET /api/hello returns Hello World``() =
    task {
        // Arrange
        let ctx = DefaultHttpContext()
        ctx.Request.Path <- PathString("/api/hello")
        ctx.Request.Method <- "GET"
        
        let app = route "/api/hello" >=> text "Hello World"
        
        // Act
        let! result = app (Some >> Task.FromResult) ctx
        
        // Assert
        match result with
        | Some ctx -> 
            let body = getBody ctx
            Assert.AreEqual("Hello World", body)
        | None -> Assert.Fail("No response produced")
    }

Performance Optimization

Giraffe is built on top of ASP.NET Core and inherits its excellent performance characteristics. Here are some additional optimizations:

Memory Management

Use ArrayPool to manage memory for large payloads:

open System.Buffers

let largeResponseHandler =
    fun (next : HttpFunc) (ctx : Microsoft.AspNetCore.Http.HttpContext) ->
        task {
            let buffer = ArrayPool<byte>.Shared.Rent(8192)
            try
                // Use buffer for processing
                // ...
                return! next ctx
            finally
                ArrayPool<byte>.Shared.Return(buffer)
        }

JSON Serialization

Optimize JSON serialization by using System.Text.Json:

open System.Text.Json
open System.Text.Json.Serialization

let configureServices (services: IServiceCollection) =
    let jsonOptions = JsonSerializerOptions()
    jsonOptions.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase
    jsonOptions.Converters.Add(JsonFSharpConverter())
    
    services.AddSingleton<Json.ISerializer>(SystemTextJson.Serializer(jsonOptions)) |> ignore
    services.AddGiraffe() |> ignore

Real-World Example: RESTful API with Database Integration

Here’s a more comprehensive example that includes database integration with Entity Framework Core:

open System
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.Extensions.Hosting
open Microsoft.Extensions.DependencyInjection
open Microsoft.EntityFrameworkCore
open Giraffe
open FSharp.Control.Tasks.V2.ContextInsensitive

// Domain models
[<CLIMutable>]
type Customer = {
    Id: int
    Name: string
    Email: string
    DateCreated: DateTime
}

// DbContext
type ApiContext(options: DbContextOptions<ApiContext>) =
    inherit DbContext(options)
    
    [<DefaultValue>]
    val mutable private _customers: DbSet<Customer>
    
    member this.Customers
        with get() = this._customers
        and set v = this._customers <- v

// Repository
type ICustomerRepository =
    abstract member GetAll: unit -> Task<Customer list>
    abstract member GetById: int -> Task<Customer option>
    abstract member Create: Customer -> Task<Customer>
    abstract member Update: int * Customer -> Task<Customer option>
    abstract member Delete: int -> Task<bool>

type CustomerRepository(ctx: ApiContext) =
    interface ICustomerRepository with
        member this.GetAll() =
            task {
                let! customers = ctx.Customers.ToListAsync()
                return customers |> Seq.toList
            }
            
        member this.GetById(id) =
            task {
                let! customer = ctx.Customers.FindAsync(id)
                return if isNull customer then None else Some customer
            }
            
        member this.Create(customer) =
            task {
                ctx.Customers.Add(customer) |> ignore
                let! _ = ctx.SaveChangesAsync()
                return customer
            }
            
        member this.Update(id, customer) =
            task {
                let! existing = (this :> ICustomerRepository).GetById(id)
                match existing with
                | Some _ ->
                    let customerWithId = { customer with Id = id }
                    ctx.Customers.Update(customerWithId) |> ignore
                    let! _ = ctx.SaveChangesAsync()
                    return Some customerWithId
                | None -> return None
            }
            
        member this.Delete(id) =
            task {
                let! existing = (this :> ICustomerRepository).GetById(id)
                match existing with
                | Some customer ->
                    ctx.Customers.Remove(customer) |> ignore
                    let! _ = ctx.SaveChangesAsync()
                    return true
                | None -> return false
            }

// HTTP handlers
module Handlers =
    let getCustomers (repository: ICustomerRepository) : HttpHandler =
        fun (next : HttpFunc) (ctx : Microsoft.AspNetCore.Http.HttpContext) ->
            task {
                let! customers = repository.GetAll()
                return! json customers next ctx
            }
            
    let getCustomerById (repository: ICustomerRepository) (id: int) : HttpHandler =
        fun (next : HttpFunc) (ctx : Microsoft.AspNetCore.Http.HttpContext) ->
            task {
                let! customer = repository.GetById(id)
                match customer with
                | Some c -> return! json c next ctx
                | None -> return! (setStatusCode 404 >=> json {| error = "Customer not found" |}) next ctx
            }
            
    let createCustomer (repository: ICustomerRepository) : HttpHandler =
        fun (next : HttpFunc) (ctx : Microsoft.AspNetCore.Http.HttpContext) ->
            task {
                let! customer = ctx.BindJsonAsync<Customer>()
                let customer = { customer with DateCreated = DateTime.UtcNow }
                let! created = repository.Create(customer)
                return! (setStatusCode 201 >=> json created) next ctx
            }
            
    let updateCustomer (repository: ICustomerRepository) (id: int) : HttpHandler =
        fun (next : HttpFunc) (ctx : Microsoft.AspNetCore.Http.HttpContext) ->
            task {
                let! customer = ctx.BindJsonAsync<Customer>()
                let! updated = repository.Update(id, customer)
                match updated with
                | Some c -> return! json c next ctx
                | None -> return! (setStatusCode 404 >=> json {| error = "Customer not found" |}) next ctx
            }
            
    let deleteCustomer (repository: ICustomerRepository) (id: int) : HttpHandler =
        fun (next : HttpFunc) (ctx : Microsoft.AspNetCore.Http.HttpContext) ->
            task {
                let! deleted = repository.Delete(id)
                if deleted then
                    return! (setStatusCode 204 >=> text "") next ctx
                else
                    return! (setStatusCode 404 >=> json {| error = "Customer not found" |}) next ctx
            }

// Configure app
let configureApp (app : IApplicationBuilder) =
    let customerRoutes (repository: ICustomerRepository) =
        choose [
            GET >=> choose [
                route "" >=> Handlers.getCustomers repository
                routef "/%i" (Handlers.getCustomerById repository)
            ]
            POST >=> route "" >=> Handlers.createCustomer repository
            PUT >=> routef "/%i" (Handlers.updateCustomer repository)
            DELETE >=> routef "/%i" (Handlers.deleteCustomer repository)
        ]
        
    let webApp =
        choose [
            subRoutef "/api/customers" (fun _ -> 
                let repository = app.ApplicationServices.GetService<ICustomerRepository>()
                customerRoutes repository)
            setStatusCode 404 >=> text "Not Found"
        ]
        
    app.UseGiraffe webApp

let configureServices (services: IServiceCollection) =
    services.AddDbContext<ApiContext>(fun options ->
        options.UseInMemoryDatabase("CustomersDb") |> ignore) |> ignore
    services.AddScoped<ICustomerRepository, CustomerRepository>() |> ignore
    services.AddGiraffe() |> ignore

[<EntryPoint>]
let main args =
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(fun webHostBuilder ->
            webHostBuilder
                .Configure(configureApp)
                .ConfigureServices(configureServices)
                |> ignore)
        .Build()
        .Run()
    0

Best Practices for Giraffe REST APIs

Functional Domain Modeling

Leverage F#’s discriminated unions and records for powerful domain modeling:

type CustomerId = CustomerId of Guid

type Address = {
    Street: string
    City: string
    PostalCode: string
    Country: string
}

type Customer = {
    Id: CustomerId
    Name: string
    Email: string
    Address: Address option
    DateCreated: DateTime
}

type OrderStatus = 
    | Pending
    | Processing
    | Shipped
    | Delivered
    | Cancelled

type OrderLine = {
    ProductId: int
    Quantity: int
    UnitPrice: decimal
}

type Order = {
    Id: Guid
    CustomerId: CustomerId
    OrderLines: OrderLine list
    Status: OrderStatus
    DateCreated: DateTime
}

HTTP Result Patterns

Use Result types for cleaner error handling:

type ApiError =
    | NotFound of string
    | ValidationError of string
    | UnauthorizedAccess
    | InternalError of string

type ApiResult<'T> = Result<'T, ApiError>

// Handler that works with the Result pattern
let apiResultHandler (result: ApiResult<'T>) : HttpHandler =
    match result with
    | Ok data -> json data
    | Error (NotFound msg) -> setStatusCode 404 >=> json {| error = msg |}
    | Error (ValidationError msg) -> setStatusCode 400 >=> json {| error = msg |}
    | Error UnauthorizedAccess -> setStatusCode 401 >=> json {| error = "Unauthorized access" |}
    | Error (InternalError msg) -> setStatusCode 500 >=> json {| error = msg |}

// Example use
let getCustomerHandler (repository: ICustomerRepository) (id: int) : HttpHandler =
    fun (next : HttpFunc) (ctx : Microsoft.AspNetCore.Http.HttpContext) ->
        task {
            let! customer = repository.GetById(id)
            let result = 
                match customer with
                | Some c -> Ok c
                | None -> Error (NotFound(sprintf "Customer with id %d not found" id))
            return! apiResultHandler result next ctx
        }

Documentation with Swagger/OpenAPI

Integrate Swagger with Giraffe using the Swashbuckle package:

open Swashbuckle.AspNetCore.Swagger

let configureServices (services: IServiceCollection) =
    // Add Giraffe
    services.AddGiraffe() |> ignore
    
    // Add Swagger
    services.AddSwaggerGen(fun c ->
        c.SwaggerDoc("v1", Microsoft.OpenApi.Models.OpenApiInfo(
            Title = "Giraffe API",
            Version = "v1",
            Description = "REST API built with Giraffe"
        ))
    ) |> ignore

let configureApp (app : IApplicationBuilder) =
    app.UseSwagger() |> ignore
    app.UseSwaggerUI(fun c ->
        c.SwaggerEndpoint("/swagger/v1/swagger.json", "Giraffe API V1")
    ) |> ignore
    app.UseGiraffe webApp

Comparison with Other .NET REST Frameworks

FeatureGiraffeASP.NET Core MVCMinimal APIs
Programming ModelFunctionalObject-OrientedLightweight, mixed
Language FocusF#C#C#
ComposabilityHighMediumMedium-High
Type SafetyHighMediumMedium
PerformanceExcellentGoodExcellent
Learning CurveSteep for OOP developersModerateLow
Community SizeSmall but dedicatedVery largeGrowing
MaturityStableVery matureNewer

Conclusion

Giraffe provides a compelling functional approach to building REST APIs in the .NET ecosystem. By combining F#’s powerful type system with ASP.NET Core’s performance and infrastructure, Giraffe enables developers to create safer, more maintainable web services with elegant and concise code.

The library is particularly well-suited for teams that:

  • Want to embrace functional programming principles
  • Value strong type safety and compile-time correctness
  • Need high performance and scalability
  • Appreciate composable, testable code

Whether you’re building a small microservice or a complex API system, Giraffe’s functional composition model provides a flexible and powerful foundation for your REST API development needs.