Skip to content

Instantly share code, notes, and snippets.

@ninjarobot
Last active January 17, 2019 13:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ninjarobot/96103006466d97e7cbc50456e0b5c5e2 to your computer and use it in GitHub Desktop.
Save ninjarobot/96103006466d97e7cbc50456e0b5c5e2 to your computer and use it in GitHub Desktop.
Composing web handlers with type safety in Suave or Giraffe

Type Safe Composition of HTTP Handlers in Suave and Giraffe

Suave and Giraffe are functional web frameworks that work based on composition of handlers. A handler is a function that accepts an HttpContext and returns an Async<HttpContext option>, fitting nicely into an HTTP server's protocol of accepting HTTP messages with a request and some metadata, then returning a message with that metadata and a response. This makes the WebPart in Suave, for example.

While the frameworks themselves only allow you to put a WebPart into the processing pipeline, it is entirely up to you how you compose functions together to get that WebPart. When you compose WebPart A and WebPart B (or HttpHandler A and HttpHandler B), you are using the a compose operator - >=>. Take a look at the function:

let compose (first : 'a -> Async<'b option>) (second : 'b -> Async<'c option>)
            : 'a -> Async<'c option>

Leaving out the async part, it accepts a function that goes from 'a to 'b option and composes it with a function that goes from 'b to 'c option. (edited)

All composed together, you certainly need your to end up WebPart that has a type HttpContext -> Async<HttpContext option> but you can compose other functions that don’t take that as input and output.

type Whatever = {
    Thing1: string
    Thing2: int
    Ctx: HttpContext
}

module ExampleTypedWebParts =
    let webPartA =
        fun (ctx:HttpContext) ->
            async {
                return { Thing1 = "part A"; Thing2 = 1; Ctx=ctx } |> Some
            }

    let webPartB =
        fun (what:Whatever) ->
            async {
                // Do something with what
                let responseContent = sprintf "thing 1 is %s and thing 2 is %i" what.Thing1 what.Thing2
                return! Successful.OK responseContent what.Ctx
            }
            
    let myApp =
        // composed together, webPartA >=> webPartB has the signature
        // HttpContext -> Async<HttpContext option> so it plugs in nicely
        Filters.GET >=> path "/what" >=> webPartA >=> webPartB
        
    startWebServer defaultConfig myApp

Between webPartA and webPartB, you have types that can carry whatever data you like, and the compiler enforces that you compose these together safely.

The only real requirement is that to plug into the rest of the framework, the whole composition of WebParts needs to have the HttpContext -> Async<HttpContext> signature. In myApp above, once webPartA and webPartB are composed together with >=>, you end up with a function with that very signature.

Parts of the composition (applicatives) can do whatever you like. A really good use of this is to make sure that some parts are only downstream of others. For instance, they may only accept something type like an AuthenticatedContext that holds both the HttpContext and some information that would have come from an authentication check, ensuring that no one ever composes them in the wrong order.

There is nothing in Suave or Giraffe that forces you to use custom types between composed functions - it only has the minimal type safety that is required to process HTTP messages. You can add all the type safety you want as long as you compose a function that meets the minimal requirement for the server to handle HTTP.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment