Composition & Middleware
Learn how to compose handlers, build middleware pipelines, and create reusable components.
Understanding WebPart
A WebPart is the basic building block in Suave:
open Suave
// A WebPart is a function that takes HttpContext and returns an async option
type WebPart = HttpContext -> Async<HttpContext option>
// If Some ctx is returned, the handler succeeded
// If None is returned, the handler failed and the next handler is tried
The pipe-forward Operator (>=>)
Chain handlers together using the >=> operator:
open Suave
open Suave.Operators
open Suave.Writers
open Suave.Successful
// Apply headers, then return OK
let app =
setHeader "X-Custom" "value" >=>
setHeader "Content-Type" "text/plain" >=>
OK "Hello World"
The choose Combinator
Try handlers in order until one succeeds:
open Suave
open Suave.Operators
open Suave.Filters
open Suave.Successful
let app =
choose [
path "/hello" >=> OK "Hello"
path "/goodbye" >=> OK "Goodbye"
RequestErrors.NOT_FOUND "Not found"
]
// The first matching path wins
// If path "/hello" fails, try path "/goodbye"
// If both fail, return NOT_FOUND
Creating Middleware
Build reusable middleware components:
open Suave
open Suave.Operators
open Suave.Writers
open Suave.Successful
open Suave.Filters
open Suave.ServerErrors
// Logging middleware
let logRequest : WebPart =
fun ctx ->
printfn "[%s] %s %s" (System.DateTime.Now.ToString()) ctx.request.rawMethod ctx.request.path
async { return Some ctx }
// Authentication middleware
let requireAuth : WebPart =
fun ctx ->
match ctx.request.header "Authorization" with
| Choice1Of2 auth -> async { return Some ctx }
| Choice2Of2 _ -> async { return None } // Fails, try next handler
// Error handling middleware
let errorHandler (ex: System.Exception) : WebPart =
fun ctx ->
printfn "Error: %s" ex.Message
INTERNAL_ERROR ex.Message ctx
// Use middleware
let app =
choose [
logRequest >=> path "/public" >=> OK "Public area"
logRequest >=> requireAuth >=> path "/admin" >=> OK "Admin area"
]
Middleware Pipeline
Build complex middleware pipelines:
open Suave
open Suave.Operators
open Suave.Writers
open Suave.Successful
open Suave.Filters
// Common middleware stack
let globalMiddleware =
setHeader "X-Frame-Options" "SAMEORIGIN" >=>
setHeader "X-Content-Type-Options" "nosniff" >=>
setHeader "X-XSS-Protection" "1; mode=block"
// CORS middleware
let corsMiddleware =
setHeader "Access-Control-Allow-Origin" "*" >=>
setHeader "Access-Control-Allow-Methods" "GET, POST, PUT, DELETE"
// Logging middleware
let loggingMiddleware : WebPart =
fun ctx ->
async {
printfn "[%s] %s %s" (System.DateTime.UtcNow.ToString()) ctx.request.rawMethod ctx.request.path
return Some ctx
}
// Apply all middleware
let app =
globalMiddleware >=>
loggingMiddleware >=>
corsMiddleware >=>
choose [
path "/" >=> OK "Home"
path "/api" >=> OK "API"
]
Conditional Composition
Conditionally compose handlers:
open Suave
open Suave.Operators
open Suave.Filters
open Suave.Successful
let isDev = System.Environment.GetEnvironmentVariable("ENV") = "Development"
let app =
choose [
// Development-only routes
if isDev then
yield path "/debug" >=> OK "Debug info"
yield path "/metrics" >=> OK "Metrics"
// Always available
yield path "/" >=> OK "Home"
// API routes
yield path "/api/users" >=> OK "Users"
]
Scoped Middleware
Apply middleware to specific routes:
open Suave
open Suave.Operators
open Suave.Filters
open Suave.Writers
open Suave.Successful
let adminMiddleware =
setHeader "X-Admin" "true" >=>
fun ctx ->
match ctx.request.header "Authorization" with
| Choice1Of2 _ -> async { return Some ctx }
| _ -> async { return None }
let apiMiddleware =
setHeader "Content-Type" "application/json" >=>
setHeader "Access-Control-Allow-Origin" "*"
let app =
choose [
// Admin routes with auth
path "/admin" >=> adminMiddleware >=> OK "Admin Dashboard"
// API routes with JSON headers
path "/api" >=> apiMiddleware >=> OK "{\"message\":\"API\"}"
// Public routes
path "/" >=> OK "Public Home"
]
Higher-Order Handlers
Create handlers that return handlers (higher-order functions):
open Suave
open Suave.Operators
open Suave.Writers
open Suave.Successful
// Returns a handler that requires a specific header
let requireHeader name value : WebPart =
fun ctx ->
match ctx.request.header name with
| Choice1Of2 headerValue when headerValue = value ->
async { return Some ctx }
| _ ->
RequestErrors.FORBIDDEN (sprintf "Missing required header: %s" name) ctx
// Returns a handler that sets a header
let withHeader name value : WebPart =
setHeader name value
// Returns a handler that applies a transformation
let withTransform (transform: string -> string) : WebPart =
fun ctx ->
async {
let original = ctx.request.path
let transformed = transform original
printfn "Transformed %s to %s" original transformed
return Some ctx
}
// Usage
let app =
choose [
requireHeader "X-API-Key" "secret" >=>
withHeader "X-Authenticated" "true" >=>
OK "Authenticated"
]
Composing Multiple Applications
Combine multiple applications into one:
open Suave
open Suave.Operators
open Suave.Filters
open Suave.Successful
// Sub-application 1: API
let apiApp =
choose [
path "/users" >=> OK "Users API"
path "/posts" >=> OK "Posts API"
]
// Sub-application 2: Admin
let adminApp =
choose [
path "/dashboard" >=> OK "Admin Dashboard"
path "/settings" >=> OK "Settings"
]
// Sub-application 3: Public
let publicApp =
choose [
path "/" >=> OK "Home"
path "/about" >=> OK "About"
]
// Combine into main app (pathStarts matches a prefix)
let app =
choose [
pathStarts "/api" >=> apiApp
pathStarts "/admin" >=> adminApp
publicApp
]
Error Handling Composition
Compose error handlers:
open Suave
open Suave.Operators
open Suave.Filters
open Suave.Successful
open Suave.ServerErrors
// Error handler wrapper
let withErrorHandling (handler: WebPart) : WebPart =
fun ctx ->
async {
try
match! handler ctx with
| Some ctx -> return Some ctx
| None ->
// Handler failed, return error
return! RequestErrors.NOT_FOUND "Not found" ctx
with
| ex ->
printfn "Unhandled error: %s" ex.Message
return! INTERNAL_ERROR "Server error" ctx
}
let app =
withErrorHandling (
choose [
path "/" >=> OK "Home"
path "/error" >=> fun _ -> failwith "Intentional error"
]
)