Requests & Responses

Learn how to handle different request types and build proper responses in Suave.

HTTP Response Types

Suave provides helpers for common HTTP responses:

open Suave
open Suave.Successful
open Suave.RequestErrors
open Suave.ServerErrors
open Suave.Filters
open Suave.Operators

let app =
  choose [
    path "/ok" >=> OK "200 OK"
    path "/created" >=> CREATED "201 Created"
    path "/accepted" >=> ACCEPTED "202 Accepted"
    
    path "/bad-request" >=> BAD_REQUEST "400 Bad Request"
    path "/unauthorized" >=> UNAUTHORIZED "401 Unauthorized"
    path "/forbidden" >=> FORBIDDEN "403 Forbidden"
    path "/not-found" >=> NOT_FOUND "404 Not Found"
    path "/conflict" >=> CONFLICT "409 Conflict"
    
    path "/server-error" >=> INTERNAL_ERROR "500 Server Error"
    path "/not-implemented" >=> NOT_IMPLEMENTED "501 Not Implemented"
  ]

Reading Request Headers

Access HTTP headers from the request:

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

let app =
  choose [
    path "/headers" >=> fun ctx ->
      let userAgent = 
        match ctx.request.header "user-agent" with
        | Choice1Of2 ua -> sprintf "User-Agent: %s" ua
        | Choice2Of2 _ -> "User-Agent not provided"
      
      let contentType =
        match ctx.request.header "content-type" with
        | Choice1Of2 ct -> sprintf "Content-Type: %s" ct
        | Choice2Of2 _ -> "Content-Type not provided"
      
      let response = sprintf "%s\n%s" userAgent contentType
      OK response ctx
  ]

Query String Parameters

Extract and parse query parameters:

open Suave
open Suave.Filters
open Suave.Operators
open Suave.Successful
open Suave.RequestErrors

let app =
  choose [
    path "/search" >=> fun ctx ->
      match ctx.request.queryParam "q" with
      | Choice1Of2 query -> 
          OK (sprintf "Search results for: %s" query) ctx
      | Choice2Of2 _ -> 
          BAD_REQUEST "Missing 'q' parameter" ctx
    
    path "/page" >=> fun ctx ->
      match ctx.request.queryParam "n" with
      | Choice1Of2 nStr when System.Int32.TryParse(nStr) |> fst ->
          let page = System.Int32.Parse(nStr)
          OK (sprintf "Page: %d" page) ctx
      | _ -> 
          BAD_REQUEST "Invalid page number" ctx
  ]

Request Body (Form Data)

Read form data from POST requests:

open Suave
open Suave.Filters
open Suave.Operators
open Suave.Successful
open Suave.RequestErrors

let app =
  choose [
    POST >=> path "/form" >=> fun ctx ->
      async {
        match ctx.request.formData "name" with
        | Choice1Of2 name ->
            let response = sprintf "Hello, %s!" name
            return! OK response ctx
        | Choice2Of2 _ ->
            return! BAD_REQUEST "Missing 'name' field" ctx
      }
  ]

JSON Request/Response

Work with JSON using the Suave.Json package (dotnet add package Suave.Json). toJson/fromJson use DataContractJsonSerializer, so mark records with DataContract:

open System.Runtime.Serialization
open Suave
open Suave.Filters
open Suave.Operators
open Suave.Successful
open Suave.RequestErrors
open Suave.Writers
open Suave.Json

[<DataContract>]
type User =
  { [<field: DataMember(Name = "id")>] id : int
    [<field: DataMember(Name = "name")>] name : string
    [<field: DataMember(Name = "email")>] email : string }

let app =
  choose [
    POST >=> path "/api/users" >=> fun ctx ->
      try
        let user = fromJson<User> ctx.request.rawForm
        OK (sprintf "Created user: %s" user.name) ctx
      with
      | _ -> BAD_REQUEST "Invalid JSON" ctx

    GET >=> path "/api/users" >=> fun ctx ->
      let users =
        [ { id = 1; name = "Alice"; email = "alice@example.com" }
          { id = 2; name = "Bob"; email = "bob@example.com" } ]
      (toJson users |> Successful.ok >=> setMimeType "application/json") ctx
  ]

Setting Response Headers

Add custom headers to responses:

open Suave
open Suave.Filters
open Suave.Operators
open Suave.Successful
open Suave.Writers

let app =
  choose [
    path "/custom-headers" >=>
      setHeader "X-Custom-Header" "CustomValue" >=>
      setHeader "Cache-Control" "no-cache" >=>
      OK "Response with custom headers"

    path "/json-api" >=>
      setMimeType "application/json; charset=utf-8" >=>
      OK "{\"message\":\"Hello\"}"
  ]

Setting Response Cookies

Add cookies to responses:

open Suave
open Suave.Writers
open Suave.Filters
open Suave.Operators
open Suave.Successful

let app =
  choose [
    path "/set-cookie" >=>
      setHeader "Set-Cookie" "session=abc123; Path=/" >=>
      OK "Cookie set"
  ]

Complete CRUD Example

A complete example with multiple request types:

open Suave
open Suave.Operators
open Suave.Filters
open Suave.Successful
open Suave.RequestErrors

type Product = { id: int; name: string; price: float }

let mutable products = 
  [ { id = 1; name = "Widget"; price = 9.99 }
    { id = 2; name = "Gadget"; price = 19.99 } ]

let app =
  choose [
    // GET /products - List all
    GET >=> path "/products" >=> 
      OK (sprintf "Products: %A" products)
    
    // GET /products/:id - Get one
    GET >=> pathScan "/products/%d" (fun id ->
      match products |> List.tryFind (fun p -> p.id = id) with
      | Some p -> OK (sprintf "Product: %s - $%.2f" p.name p.price)
      | None -> NOT_FOUND "Product not found"
    )
    
    // POST /products - Create
    POST >=> path "/products" >=> fun ctx ->
      match ctx.request.formData "name", ctx.request.formData "price" with
      | Choice1Of2 name, Choice1Of2 priceStr when System.Double.TryParse(priceStr) |> fst ->
          let price = System.Double.Parse(priceStr)
          let id = products |> List.maxBy (fun p -> p.id) |> fun p -> p.id + 1
          let product = { id = id; name = name; price = price }
          products <- products @ [product]
          CREATED (sprintf "Created: %s" name) ctx
      | _ -> BAD_REQUEST "Invalid data" ctx
  ]