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"
    ]
  )