Yet Another F# Web Framework

Update Jan 1, 2019

I spent some time splitting the projects and updating to ASP.NET Core 3.0 alpha (preview targets netcoreapp3.0 only). The Endpoint Routing feature is really, really nice. You can see the updated version here (diff).

Original Post

While evaluating the state of F# web frameworks over the holidays, I managed to create yet another lightweight framework prototype. You’re welcome. The prototype is based on ASP.NET Core Routing and some posts by Filip Wojcieszyn, specifically Building microservices with ASP.NET Core (without MVC) and Running ASP.NET Core content negotiation by hand. The prototype currently consists of a ContentNegotiation module and a Builder module containing a RouterBuilder and a ResourceBuilder, where the two builder types are computation expressions using CustomOperationAttributes.

let helloName =
    resource app.ApplicationServices "hello/{name}" {
        name "Hello Name"

        get (fun ctx ->
            let name = ctx.GetRouteValue("name") |> string
            ctx.Response.WriteAsync(sprintf "Hi, %s!" name))

        put (fun ctx ->
            let name = ctx.GetRouteValue("name") |> string
            ContentNegotiation.negotiate 201 name ctx)

The above helloName is typed as a Microsoft.AspNetCore.Routing.IRouter. In fact, it’s an INamedRouter named “Hello Name” and is applied to the current IApplicationBuilder. It registers a handlers for GET and POST requests for urls matching hello/{name}. I think it would be nice to add Giraffe to compose the handler functions.

The ResourceBuilder is opinionated in that it assumes a resource-per-url-template, and it takes advantage of the method overloading trick I discovered while at Open F# to let you register handlers returning Task, Task<'a>, Async<'a>, or unit. This provides a lot of flexibility in the builder’s ability to adapt to existing functions. Here’s another resource definition:

let hello =
    resource app.ApplicationServices "hello" {
        name "Hello"

        // Using HttpContext -> () overload
        get (fun ctx ->
            use writer = new System.IO.StreamWriter(ctx.Response.Body)
            writer.Write("Hello, world!")

        // Using HttpContext -> Task<'a> overload
        post (fun ctx ->
            task {
                if ctx.Request.HasFormContentType then
                    let! form = ctx.Request.ReadFormAsync()
                    ctx.Response.StatusCode <- 201
                    use writer = new System.IO.StreamWriter(ctx.Response.Body)
                    do! writer.WriteLineAsync("Received form data:")
                    for KeyValue(key, value) in form do
                        do! writer.WriteLineAsync(sprintf "%s: %A" key (value.ToArray()))
                    do! writer.FlushAsync()
                elif ctx.Request.ContentType = "application/json" then
                    ctx.Request.Body.Seek(0L, System.IO.SeekOrigin.Begin) |> ignore
                    use reader = new System.IO.StreamReader(ctx.Request.Body)
                    let! input = reader.ReadToEndAsync()
                    let json = JObject.Parse input
                    do! ContentNegotiation.negotiate 201 json ctx
                    ctx.Response.StatusCode <- 500
                    do! ctx.Response.WriteAsync("Could not seek")

This one does a lot more, mostly to show off different types of return types. However, it’s essentially returning Hello, world! on GET requests to hello and returns the payload as text/plain on a POST request.

The router computation expression allows these resources to be mounted and can also plug in additional middleware:

router app {
    plugWhen (env.IsDevelopment()) DeveloperExceptionPageExtensions.UseDeveloperExceptionPage
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see
    plugWhenNot (env.IsDevelopment()) HstsBuilderExtensions.UseHsts

    plug HttpsPolicyBuilderExtensions.UseHttpsRedirection
    plug ResponseCachingExtensions.UseResponseCaching
    plug ResponseCompressionBuilderExtensions.UseResponseCompression
    plug StaticFileExtensions.UseStaticFiles

    route helloName
    route hello

Amazingly, this works. The Builder.fs file is 207 lines long, most of which consists of method overloads. Additional improvements could include adapting the router builder to encompass the entire WebHostBuilder, which would allow registering related services in addition to the corresponding middleware.

Now the obvious question: is this needed?


Almost a decade ago, I created a little library named Frank, inspired by Sinatra, for building web apps with F#. My original goal was to write something lightweight that plugged in well with the available .NET infrastructure. It so happened that WCF Web API — later ASP.NET Web API — and OWIN developed at the same time, so Frank evolved to work with those types and hosting models. This was somewhat helpful later when Azure Functions used the same types for its HttpTrigger.

Here’s a hello world request handler with Frank, using HttpRequestMessage -> Async<HttpResponseMessage> as the common signature:

let helloWorld request =
    OK ignore <| Str "Hello, world!"
    |> async.Return

Here’s a middleware with Frank, using (HttpRequestMessage -> Async<HttpResponseMessage>) -> HttpRequestMessage -> Async<HttpResponseMessage> as the signature:

let log app = fun (request : HttpRequestMessage) -> async {
    let sw = System.Diagnostics.Stopwatch.StartNew()
    let! response = app request
    printfn "Received a %A request from %A. Responded in %i ms."
    return response

Suave also appeared around the same time and provided a similar, but slightly different approach to combining the pieces (from

let greetings q =
  defaultArg (Option.ofChoice (q ^^ "name")) "World" |> sprintf "Hello %s"

let sample : WebPart = 
    path "/hello" >=> choose [
      GET  >=> request (fun r -> OK (greetings r.query))
      POST >=> request (fun r -> OK (greetings r.form))
      RequestErrors.NOT_FOUND "Found no handlers" ]

I find the WebParts, or HttpContext -> Async<HttpContext option>, a nicer design than what I was trying in Frank. The only hang up I had was that Suave didn’t initially have any ties into the existing .NET web tools. I also started doubling down on my use of OWIN, and since there weren’t many (if any) people using Frank, I abandoned it to its fate.

I started looking at Type Providers in the F# 3.0 time frame, specifically using a specification to generate an OWIN application. This quickly evolved into working with Andrew Cherry on Freya instead, a webmachine-inspired framework built on OWIN and using the CustomOperationAttribute and computation expressions. This was eye-opening, as Freya used several functional data structures such as Lenses, Prisms, Optics, Graphs, and even did tree-shaking on the composed graphs to optimize performance. I was amazed to learn what could be achieved by computation expressions.

Freya exposed hooks for plugging in functionality rather than function composition. This, too, was rather exciting to see.

let users =
    freyaMachine {
        methods [ GET; OPTIONS; POST ]
        availableMediaTypes MediaType.json
        doPost createUser
        handleOk listUsers }

OWIN defined no pipeline for processing anything, so Freya providing the HTTP state machine graph was a fantastic addition for an app building directly on top of OWIN.

Since this time, Giraffe and Saturn have popped up, and the venerable WebSharper has continued to evolve. All of these are excellent frameworks, each offering something slightly different.

Choices and Revelations

Recently, I found I needed to start writing some new APIs and wondered which approach I should take. I have frequently just written things directly in OWIN handlers. However, I’m trying to move to ASP.NET Core and thought I would take another look at what’s there. As a baseline, I considered Freya, Giraffe, and WebSharper. I also thought about ASP.NET Core Web API, which has a lot to offer but uses an unnecessary OO style that I prefer to avoid.

One of my team members started with Suave, and another converted it to a Giraffe web app. In the process of reviewing it and the Giraffe source code, I realized that Giraffe solves the problem I tried to solve with Frank but using the nicer design of Suave. However, it still doesn’t tie completely into the ASP.NET Core pipeline. Nevertheless, it does an excellent job of leveraging the infrastructure while remaining very fast and providing terrific abstractions.

I wanted to start with Freya, as I’ve had a small part in bringing that to life and like the approach a lot. However, I still find I struggle to use it correctly, and I had a revelation while thinking about its use of the HTTP decision graph. The Freya graph acts as a pipeline for choosing the correct request handler. ASP.NET Core does the same using its Routing infrastructure. For some reason, probably because I’ve been stuck thinking in terms of OWIN, I’d forgotten that the ASP.NET Core Routing pipeline serves essentially the same purpose. This revelation was helped by catching up on some of the upcoming changes in 2.2 (for MVC only) and 3.0, specifically Endpoint Routing (Community Standup video).

Endpoint Routing appears to resolve the big problem of wanting to leverage the ASP.NET Core infrastructure while not requiring use of ASP.NET Core MVC or Web API. This means Open API generation, API documentation, test clients, and more could all come from a single source, regardless of framework choice.

Next Steps

I’m planning on updating these builders to work with Endpoint Routing in ASP.NET Core 3.0. This may end up being somewhat useful as a means of working Endpoint Routing back into Giraffe, which appears to be a desired feature change, specifically for enabling Open API generation.

In the meantime, I’ll probably end up continuing with the Giraffe trajectory. It satisfies our desire to move to .NET Core and has great abstractions for composition. It’s also very likely the builders I’ve created could easily be adapted to work with Giraffe.

Source code for this post is available at


2 thoughts on “Yet Another F# Web Framework

Comments are closed.