Frank


Getting Started

In this tutorial, we'll explore the basics of building and hosting Web APIs with Frank.

Define an Application

One may define a web application interface using a large variety of signatures. Indeed, if you search the web, you're likely to find a large number of approaches. When starting with Frank, I wanted to try to find a way to define an HTTP application using pure functions and function composition. The closest I found was the following:

1: 
2: 
3: 
4: 
type HttpApplication = HttpRequestMessage -> Async<HttpResponseMessage>
    
let orElse left right = fun request -> Option.orElse (left request) (right request)
let inline (<|>) left right = orElse left right

The last of these was a means for merging multiple applications together into a single application. This allowed for a nice symmetry and elegance in that everything you composed would always have the same signature. Additional functions would allow you to map applications to specific methods or uri patterns.

A "Hello, world!" application using these signatures would look like the following:

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

A simple echo handler that returns the same input as it received might look like the following:

1: 
2: 
3: 
4: 
5: 
6: 
let echo (request: HttpRequestMessage) = async {
    let! content = Async.AwaitTask <| request.Content.ReadAsStringAsync()
    return respond HttpStatusCode.OK
           <| ``Content-Type`` "text/plain"
           <| new StringContent(content)
}

or just:

1: 
2: 
3: 
let echo (request: HttpRequestMessage) =
    OK <| ``Content-Type`` "text/plain"
       <| (Async.AwaitTask <| request.Content.ReadAsStringAsync())

If you want to provide content negotiation, use:

1: 
2: 
let echo = runConneg formatters <| fun request ->
    Async.AwaitTask <| request.Content.ReadAsStringAsync()

Define an HTTP Resource

Alas, this approach works only so well. HTTP is a rich communication specification. The simplicity and elegance of a purely functional approach quickly loses the ability to communicate back options to the client. For instance, given the above, how do you return a meaningful 405 Method Not Allowed response? The HTTP specification requires that you list the allowed methods, but if you merge all the logic for selecting an application into the functions, there is no easy way to recall all the allowed methods, short of trying them all. You could require that the developer add the list of used methods, but that, too, misses the point that the application should be collecting this and helping the developer by taking care of all of the nuts and bolts items.

The next approach I tried involved using a tuple of a list of allowed HTTP methods and the application handler, which used the merged function approach described above for actually executing the application. However, once again, there are limitations. This structure accurately represents a resource, but it does not allow for multiple resources to coexist side-by-side. Another tuple of uri pattern matching expressions could wrap a list of these method * handler tuples, but at this point I realized I would be better served by using real types and added an HttpResource type to ease the type burden.

HTTP resources expose an resource handler function at a given uri. In the common MVC-style frameworks, this would roughly correspond to a Controller. Resources should represent a single entity type, and it is important to note that a Foo is not the same entity type as a Foo list, which is where the typical MVC approach goes wrong.

The 405 Method Not Allowed function allows a resource to correctly respond to messages. Therefore, we extend the HttpResource with an Invoke method. Also note that the methods will always be looked up using the latest set. This could probably be memoized so as to save a bit of time, but it allows us to ensure that all available methods are reported.

Compose Applications and Resources into Applications

A compositional approach to type mapping and handler design. Here, the actual function shows clearly that we are really using the id function to return the very same result.

1: 
let echo2Transform = id

The echo2ReadRequest maps the incoming request to a value that can be used within the actual computation, or echo2Transform in this example.

1: 
2: 
let echo2ReadRequest (request: HttpRequestMessage) =
    Async.AwaitTask <| request.Content.ReadAsStringAsync()

The echo2Respond maps the outgoing message body to an HTTP response.

1: 
2: 
let echo2Respond body =
    respond <| ``Content-Type`` "text/plain" <| body

This echo2 is the same in principle as echo above, except that the logic for the message transform deals only with the concrete types about which it cares and isn't bothered by the transformations.

1: 
2: 
3: 
4: 
5: 
let echo2 request = async {
    let! content = echo2ReadRequest request
    let body = echo2Transform content
    return echo2Respond <| new StringContent(body)
}

Create a HttpResource instance at the root of the site that responds to POST.

1: 
let resource = route "/" <| post echo2

Other combinators are available to handle other scenarios, such as:

  1. Content Negotiation
  2. Building Responses
  3. Combining applications into resources
  4. Combining resources into applications

Check the samples for more examples of these combinators.

Define a Middleware

Middlewares follow a Russian-doll model for wrapping resource handlers with additional functionality. Frank middlewares take an HttpApplication and return an HttpApplication. The Frank.Middleware module defines several, simple middlewares, such as the log middleware that intercepts logs incoming requests and the time taken to respond:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
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."
            request.Method.Method
            request.RequestUri.PathAndQuery
            sw.ElapsedMilliseconds
    sw.Reset()
    return response
}

The most likely place to insert middlewares is the outer edge of your application. However, since middlewares are themselves just HttpApplications, you can compose them into a Frank application at any level. Want to support logging only on one troublesome resource? No problem. Expose the resource as an application, wrap it in the log middleware, and insert it into the larger application as you did before.

Hosting

Frank will run on any hosting platform that supports the Web API library. To hook up your Frank application, use the register function, passing in the resources and the instance of HttpConfiguration.

1: 
register [resource] config

This extension adds a default route to your HttpConfiguration instance and adds a DelegatingHandler instance to the route's HttpConfiguration.MessageHandlers collection.

namespace System
namespace System.Net
namespace System.Net.Http
namespace System.Web
namespace System.Web.Http
Multiple items
module HttpResource

from System.Web.Http

--------------------
type HttpResource =
  inherit HttpRoute
  new : template:string * methods:seq<HttpMethod> * handler:(HttpRequestMessage -> Async<HttpResponseMessage> option) -> HttpResource
  member Name : string

Full name: System.Web.Http.HttpResource

--------------------
new : template:string * methods:seq<HttpMethod> * handler:(HttpRequestMessage -> Async<HttpResponseMessage> option) -> HttpResource
namespace Microsoft.FSharp.Control
namespace Frank
type HttpApplication = HttpRequestMessage -> Async<HttpResponseMessage>

Full name: Tutorial.HttpApplication
Multiple items
type HttpRequestMessage =
  new : unit -> HttpRequestMessage + 2 overloads
  member Content : HttpContent with get, set
  member Dispose : unit -> unit
  member Headers : HttpRequestHeaders
  member Method : HttpMethod with get, set
  member Properties : IDictionary<string, obj>
  member RequestUri : Uri with get, set
  member ToString : unit -> string
  member Version : Version with get, set

Full name: System.Net.Http.HttpRequestMessage

--------------------
HttpRequestMessage() : unit
HttpRequestMessage(method: HttpMethod, requestUri: Uri) : unit
HttpRequestMessage(method: HttpMethod, requestUri: string) : unit
Multiple items
type Async
static member AsBeginEnd : computation:('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit)
static member AwaitEvent : event:IEvent<'Del,'T> * ?cancelAction:(unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate)
static member AwaitIAsyncResult : iar:IAsyncResult * ?millisecondsTimeout:int -> Async<bool>
static member AwaitTask : task:Task<'T> -> Async<'T>
static member AwaitWaitHandle : waitHandle:WaitHandle * ?millisecondsTimeout:int -> Async<bool>
static member CancelDefaultToken : unit -> unit
static member Catch : computation:Async<'T> -> Async<Choice<'T,exn>>
static member FromBeginEnd : beginAction:(AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg:'Arg1 * beginAction:('Arg1 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * beginAction:('Arg1 * 'Arg2 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * arg3:'Arg3 * beginAction:('Arg1 * 'Arg2 * 'Arg3 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromContinuations : callback:(('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T>
static member Ignore : computation:Async<'T> -> Async<unit>
static member OnCancel : interruption:(unit -> unit) -> Async<IDisposable>
static member Parallel : computations:seq<Async<'T>> -> Async<'T []>
static member RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:CancellationToken -> 'T
static member Sleep : millisecondsDueTime:int -> Async<unit>
static member Start : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions * ?cancellationToken:CancellationToken -> Task<'T>
static member StartChild : computation:Async<'T> * ?millisecondsTimeout:int -> Async<Async<'T>>
static member StartChildAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions -> Async<Task<'T>>
static member StartImmediate : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartWithContinuations : computation:Async<'T> * continuation:('T -> unit) * exceptionContinuation:(exn -> unit) * cancellationContinuation:(OperationCanceledException -> unit) * ?cancellationToken:CancellationToken -> unit
static member SwitchToContext : syncContext:SynchronizationContext -> Async<unit>
static member SwitchToNewThread : unit -> Async<unit>
static member SwitchToThreadPool : unit -> Async<unit>
static member TryCancelled : computation:Async<'T> * compensation:(OperationCanceledException -> unit) -> Async<'T>
static member CancellationToken : Async<CancellationToken>
static member DefaultCancellationToken : CancellationToken

Full name: Microsoft.FSharp.Control.Async

--------------------
type Async<'T>

Full name: Microsoft.FSharp.Control.Async<_>
Multiple items
type HttpResponseMessage =
  new : unit -> HttpResponseMessage + 1 overload
  member Content : HttpContent with get, set
  member Dispose : unit -> unit
  member EnsureSuccessStatusCode : unit -> HttpResponseMessage
  member Headers : HttpResponseHeaders
  member IsSuccessStatusCode : bool
  member ReasonPhrase : string with get, set
  member RequestMessage : HttpRequestMessage with get, set
  member StatusCode : HttpStatusCode with get, set
  member ToString : unit -> string
  ...

Full name: System.Net.Http.HttpResponseMessage

--------------------
HttpResponseMessage() : unit
HttpResponseMessage(statusCode: HttpStatusCode) : unit
val orElse : left:'a -> right:'b -> request:'c -> 'd

Full name: Tutorial.orElse
val left : 'a
val right : 'b
val request : 'c
module Option

from Microsoft.FSharp.Core
val orElse : left:'a list * ('b -> 'c option) -> right:'a list * ('b -> 'c option) -> 'a list * ('b -> 'c option)

Full name: System.Web.Http.HttpResource.orElse
val helloWorld : request:'a -> Async<(#HttpRequestMessage -> HttpResponseMessage)>

Full name: Tutorial.helloWorld
val request : 'a
val OK : headers:(HttpResponseMessage -> unit) -> content:#HttpContent option -> (HttpRequestMessage -> HttpResponseMessage)

Full name: Frank.Core.OK
val ignore : value:'T -> unit

Full name: Microsoft.FSharp.Core.Operators.ignore
val Str : s:string -> HttpContent

Full name: Frank.Core.Str
val async : AsyncBuilder

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.async
member AsyncBuilder.Return : value:'T -> Async<'T>
val echo : request:HttpRequestMessage -> Async<(#HttpRequestMessage -> HttpResponseMessage)>

Full name: Tutorial.echo
val request : HttpRequestMessage
val content : string
static member Async.AwaitTask : task:Threading.Tasks.Task<'T> -> Async<'T>
property HttpRequestMessage.Content: HttpContent
HttpContent.ReadAsStringAsync() : Threading.Tasks.Task<string>
val respond : statusCode:HttpStatusCode -> headers:(HttpResponseMessage -> unit) -> content:#HttpContent option -> request:HttpRequestMessage -> HttpResponseMessage

Full name: Frank.Core.respond
type HttpStatusCode =
  | Continue = 100
  | SwitchingProtocols = 101
  | OK = 200
  | Created = 201
  | Accepted = 202
  | NonAuthoritativeInformation = 203
  | NoContent = 204
  | ResetContent = 205
  | PartialContent = 206
  | MultipleChoices = 300
  ...

Full name: System.Net.HttpStatusCode
field HttpStatusCode.OK = 200
val ( Content-Type ) : x:string -> response:HttpResponseMessage -> unit

Full name: Frank.Core.( Content-Type )
Multiple items
type StringContent =
  inherit ByteArrayContent
  new : content:string -> StringContent + 2 overloads

Full name: System.Net.Http.StringContent

--------------------
StringContent(content: string) : unit
StringContent(content: string, encoding: Text.Encoding) : unit
StringContent(content: string, encoding: Text.Encoding, mediaType: string) : unit
val echo : request:HttpRequestMessage -> (HttpRequestMessage -> HttpResponseMessage)

Full name: Tutorial.echo
val echo : (HttpRequestMessage -> Async<HttpResponseMessage>)

Full name: Tutorial.echo
val runConneg : formatters:seq<Formatting.MediaTypeFormatter> -> f:(HttpRequestMessage -> Async<'a>) -> HttpApplication

Full name: Frank.Core.runConneg
val echo2Transform : ('a -> 'a)

Full name: Tutorial.echo2Transform
val id : x:'T -> 'T

Full name: Microsoft.FSharp.Core.Operators.id
val echo2ReadRequest : request:HttpRequestMessage -> Async<string>

Full name: Tutorial.echo2ReadRequest
val echo2Respond : body:(HttpResponseMessage -> unit) -> (#HttpContent option -> HttpRequestMessage -> HttpResponseMessage)

Full name: Tutorial.echo2Respond
val body : (HttpResponseMessage -> unit)
val echo2 : request:HttpRequestMessage -> Async<(#HttpContent option -> #HttpRequestMessage -> HttpResponseMessage)>

Full name: Tutorial.echo2
val body : string
val resource : HttpResource

Full name: Tutorial.resource
val route : uri:string -> handler:seq<HttpMethod> * (HttpRequestMessage -> Async<HttpResponseMessage> option) -> HttpResource

Full name: System.Web.Http.HttpResource.route
val post : handler:(HttpRequestMessage -> 'b) -> HttpMethod list * (#HttpRequestMessage -> 'b option)

Full name: System.Web.Http.HttpResource.post
val log : app:(HttpRequestMessage -> Async<'a>) -> request:HttpRequestMessage -> Async<'a>

Full name: Tutorial.log
val app : (HttpRequestMessage -> Async<'a>)
val sw : Diagnostics.Stopwatch
namespace System.Diagnostics
Multiple items
type Stopwatch =
  new : unit -> Stopwatch
  member Elapsed : TimeSpan
  member ElapsedMilliseconds : int64
  member ElapsedTicks : int64
  member IsRunning : bool
  member Reset : unit -> unit
  member Restart : unit -> unit
  member Start : unit -> unit
  member Stop : unit -> unit
  static val Frequency : int64
  ...

Full name: System.Diagnostics.Stopwatch

--------------------
Diagnostics.Stopwatch() : unit
Diagnostics.Stopwatch.StartNew() : Diagnostics.Stopwatch
val response : 'a
val printfn : format:Printf.TextWriterFormat<'T> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
property HttpRequestMessage.Method: HttpMethod
property HttpMethod.Method: string
property HttpRequestMessage.RequestUri: Uri
property Uri.PathAndQuery: string
property Diagnostics.Stopwatch.ElapsedMilliseconds: int64
Diagnostics.Stopwatch.Reset() : unit
val register : resources:seq<HttpResource> -> config:HttpConfiguration -> unit

Full name: System.Web.Http.HttpResource.register
Fork me on GitHub