Router Module

The Router module provides efficient HTTP routing with path parameter extraction and flexible route matching patterns.

Basic Router Setup

Create a simple router with basic routes:

open Suave
open Suave.Successful
open Suave.Router

let app =
  router {
    get "/" (OK "Home page")
    get "/about" (OK "About page")
    post "/submit" (OK "Form submitted")
    delete "/items/:id" (OK "Item deleted")
  }

[<EntryPoint>]
let main argv =
  startWebServer defaultConfig app
  0

Path Parameters

Extract values from URL paths using parameters:

open Suave
open Suave.Successful
open Suave.Router
open Suave.Operators

let getUser ctx =
  match routeParam "id" ctx with
  | Some id -> OK (sprintf "User %s" id) ctx
  | None -> RequestErrors.BAD_REQUEST "Missing user ID" ctx

let app =
  router {
    get "/users/:id" getUser
    get "/posts/:postId/comments/:commentId" (fun ctx ->
      match (routeParam "postId" ctx, routeParam "commentId" ctx) with
      | (Some postId, Some commentId) ->
          OK (sprintf "Post %s, Comment %s" postId commentId) ctx
      | _ -> RequestErrors.BAD_REQUEST "Missing parameters" ctx
    )
  }

[<EntryPoint>]
let main argv =
  startWebServer defaultConfig app
  0

HTTP Methods

Use different HTTP methods for different route handlers:

open Suave
open Suave.Successful
open Suave.Router
open Suave.Operators

let app =
  router {
    get "/api/users" (OK "List all users")
    post "/api/users" (OK "Create new user")
    get "/api/users/:id" (OK "Get user by ID")
    put "/api/users/:id" (OK "Update user")
    delete "/api/users/:id" (OK "Delete user")
    patch "/api/users/:id" (OK "Patch user")
    head "/api/health" (OK "")
    options "/api/users" (OK "")
  }

[<EntryPoint>]
let main argv =
  startWebServer defaultConfig app
  0

Route Scoping

Group routes with a common prefix. scope builds a Router; turn it into a WebPart with tryRoute (or put the same paths in a top-level router):

open Suave
open Suave.Successful
open Suave.Router
open Suave.Operators

let runRouter (r : Router) : WebPart =
  fun ctx -> tryRoute r ctx

let apiScope =
  scope "/api" {
    get "/users" (OK "List users")
    get "/users/:id" (OK "Get user")
    post "/users" (OK "Create user")
    delete "/users/:id" (OK "Delete user")
  }

let v2Scope =
  scope "/v2" {
    get "/users" (OK "List users (v2)")
    get "/posts" (OK "List posts (v2)")
  }

let app =
  choose [
    runRouter apiScope
    runRouter v2Scope
    router {
      get "/" (OK "Home")
    }
  ]

[<EntryPoint>]
let main argv =
  startWebServer defaultConfig app
  0

Wildcard Routes

Match remaining path segments with wildcards:

open Suave
open Suave.Successful
open Suave.Router
open Suave.Operators

let serveStatic ctx =
  match routeParam "filepath" ctx with
  | Some filepath ->
      OK (sprintf "Serving file: %s" filepath) ctx
  | None -> RequestErrors.NOT_FOUND "File not found" ctx

let app =
  router {
    get "/files/*filepath" serveStatic
    get "/docs/*filepath" (fun ctx ->
      match routeParam "filepath" ctx with
      | Some docPath ->
          OK (sprintf "Documentation: %s" docPath) ctx
      | None -> RequestErrors.NOT_FOUND "Doc not found" ctx
    )
  }

[<EntryPoint>]
let main argv =
  startWebServer defaultConfig app
  0

Complex Route Patterns

Combine multiple parameters and dynamic segments:

open Suave
open Suave.Successful
open Suave.Router
open Suave.Operators

let getCommentHandler ctx =
  match (routeParam "userId" ctx, routeParam "postId" ctx, routeParam "commentId" ctx) with
  | (Some userId, Some postId, Some commentId) ->
      let response = 
        sprintf "User %s, Post %s, Comment %s"
          userId postId commentId
      OK response ctx
  | _ ->
      RequestErrors.BAD_REQUEST "Missing required parameters" ctx

let app =
  router {
    get "/users/:userId/posts/:postId/comments/:commentId" getCommentHandler
    get "/repos/:owner/:repo/branches/:branch" (fun ctx ->
      match (routeParam "owner" ctx, routeParam "repo" ctx, routeParam "branch" ctx) with
      | (Some owner, Some repo, Some branch) ->
          OK (sprintf "%s/%s - Branch: %s" owner repo branch) ctx
      | _ -> RequestErrors.BAD_REQUEST "Missing repository parameters" ctx
    )
  }

[<EntryPoint>]
let main argv =
  startWebServer defaultConfig app
  0

Route with Middleware

Apply middleware to router handlers:

open Suave
open Suave.Successful
open Suave.Router
open Suave.Operators
open Suave.Filters

let requireJson =
  Writers.setHeader "Content-Type" "application/json"

let logRequest : WebPart =
  fun ctx ->
    printfn "Method: %s, Path: %s"
      ctx.request.rawMethod
      ctx.request.path
    async { return Some ctx }

let app =
  router {
    get "/" (logRequest >=> OK "Home")
    get "/api/data" (logRequest >=> requireJson >=> OK """{"message":"Hello"}""")
    post "/api/submit" (logRequest >=> OK "Submitted")
  }

[<EntryPoint>]
let main argv =
  startWebServer defaultConfig app
  0

RESTful API Example

Build a complete RESTful API with the Router module:

open Suave
open Suave.Router
open Suave.Operators
open Suave.Successful
open Suave.RequestErrors

let users = System.Collections.Generic.Dictionary<string, string>()

let getAllUsers = OK (System.String.Join(",", users.Keys)) 
let getUser ctx =
  match routeParam "id" ctx with
  | Some id when users.ContainsKey(id) ->
      OK (sprintf "User: %s" users.[id]) ctx
  | Some _ -> NOT_FOUND "User not found" ctx
  | None -> BAD_REQUEST "Missing user ID" ctx

let createUser = OK "User created"

let updateUser ctx = async {
  match routeParam "id" ctx with
  | Some id ->
      users.[id] <- "Updated"
      return! OK "User updated" ctx
  | None -> return! BAD_REQUEST "Missing user ID" ctx
}

let deleteUser ctx = async {
  match routeParam "id" ctx with
  | Some id when users.ContainsKey(id) ->
      users.Remove(id) |> ignore
      return! OK "User deleted" ctx
  | Some _ -> return! NOT_FOUND "User not found" ctx
  | None -> return! BAD_REQUEST "Missing user ID" ctx
}

let app =
  router {
    get "/api/users" getAllUsers
    post "/api/users" createUser
    get "/api/users/:id" getUser
    put "/api/users/:id" updateUser
    delete "/api/users/:id" deleteUser
  }

[<EntryPoint>]
let main argv =
  startWebServer defaultConfig app
  0

Nested Scopes

Prefer one router (or one scope) with the full path prefix — nested scope builders are not supported:

open Suave
open Suave.Successful
open Suave.Router
open Suave.Operators

let app =
  router {
    get "/api/v1/users" (OK "V1 Users")
    get "/api/v1/posts" (OK "V1 Posts")
    get "/api/v2/users" (OK "V2 Users")
    get "/api/v2/posts" (OK "V2 Posts")
    get "/admin/dashboard" (OK "Admin Dashboard")
    get "/admin/users" (OK "Admin Users")
    delete "/admin/users/:id" (OK "User deleted")
    get "/" (OK "Home")
  }

[<EntryPoint>]
let main argv =
  startWebServer defaultConfig app
  0

Route Parameters in Complex Handlers

Use route parameters in sophisticated request handling:

open Suave
open Suave.Router
open Suave.Operators
open Suave.Successful
open Suave.RequestErrors

let handleRequest ctx =
  async {
    // Extract multiple parameters
    let orgId = routeParam "orgId" ctx
    let projectId = routeParam "projectId" ctx
    let resourceId = routeParam "resourceId" ctx
    
    match (orgId, projectId, resourceId) with
    | (Some org, Some proj, Some res) ->
        let response = 
          sprintf 
            "Organization: %s\nProject: %s\nResource: %s"
            org proj res
        return! OK response ctx
    | _ ->
        return! BAD_REQUEST "Missing required parameters" ctx
  }

let app =
  router {
    get "/orgs/:orgId/projects/:projectId/resources/:resourceId" handleRequest
  }

[<EntryPoint>]
let main argv =
  startWebServer defaultConfig app
  0

Custom Route Builder

Build routes programmatically with the route helper:

open Suave
open Suave.Successful
open Suave.Router
open Suave.Operators

let buildApiRoute path handler =
  router {
    // Custom `route` takes one argument: (methods * path * handler)
    route ([ HttpMethod.GET; HttpMethod.POST ], path, handler)
  }

let multiMethodHandler ctx =
  match ctx.request.method with
  | HttpMethod.GET -> OK "GET request" ctx
  | HttpMethod.POST -> OK "POST request" ctx
  | _ -> OK "Other method" ctx

let app =
  choose [
    buildApiRoute "/data" multiMethodHandler
    router {
      get "/" (OK "Home")
    }
  ]

[<EntryPoint>]
let main argv =
  startWebServer defaultConfig app
  0

Route Matching Performance

The Router module optimizes performance with exact and pattern matching:

open Suave
open Suave.Successful
open Suave.Router
open Suave.Operators

// Routes with exact paths are O(1) lookups
// Routes with parameters are checked in order

let app =
  router {
    // Exact matches (fast - O(1))
    get "/" (OK "Home")
    get "/about" (OK "About")
    get "/contact" (OK "Contact")
    
    // Parameterized routes (slower - checked in order)
    get "/users/:id" (fun ctx ->
      match routeParam "id" ctx with
      | Some id -> OK (sprintf "User %s" id) ctx
      | None -> OK "No user" ctx
    )
    
    // Wildcard routes (checked last)
    get "/static/*" (fun ctx ->
      match routeParam "path" ctx with
      | Some path -> OK (sprintf "Static: %s" path) ctx
      | None -> OK "Static file" ctx
    )
  }

[<EntryPoint>]
let main argv =
  startWebServer defaultConfig app
  0

Error Handling in Routes

Handle errors and edge cases in router handlers:

open Suave
open Suave.Router
open Suave.Operators
open Suave.Successful
open Suave.RequestErrors

let safeGetUser ctx =
  async {
    try
      match routeParam "id" ctx with
      | Some id ->
          let userId = System.Int32.Parse(id)
          if userId > 0 then
            return! OK (sprintf "User %d" userId) ctx
          else
            return! BAD_REQUEST "Invalid user ID" ctx
      | None ->
          return! BAD_REQUEST "Missing user ID" ctx
    with
    | _ -> return! BAD_REQUEST "Invalid format" ctx
  }

let app =
  router {
    get "/users/:id" safeGetUser
  }

[<EntryPoint>]
let main argv =
  startWebServer defaultConfig app
  0