Giraffe
11 minute read
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
Feature | Giraffe | ASP.NET Core MVC | Minimal APIs |
---|---|---|---|
Programming Model | Functional | Object-Oriented | Lightweight, mixed |
Language Focus | F# | C# | C# |
Composability | High | Medium | Medium-High |
Type Safety | High | Medium | Medium |
Performance | Excellent | Good | Excellent |
Learning Curve | Steep for OOP developers | Moderate | Low |
Community Size | Small but dedicated | Very large | Growing |
Maturity | Stable | Very mature | Newer |
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.