Computation Expression Approach For Calling Rest Api
Computation expression approach for calling REST API
In this post, I will share with you how I handle latency and exception for Ajax calls from a front-end Single page application built in F# with WebSharper. The code is inspired by @ScottWlaschin blog post http://fsharpforfunandprofit.com/posts/elevated-world-5/#asynclist. I will not explain what is a computation expression, for that you can refer to fsharpforfunandprofit computation expressions tutorial and I will not explain how to use WebSharper and UI.Next, for that you can refer to UI.Next samples. What I will explain here is how I combine both; to write simple code without worrying about side-effects.
When interacting with REST API from a Single page application, it is a common pattern to:
- Get an authentication token with a Ajax call
- Retrieve some JSON data using the token with a Ajax call (again)
- Deserialize the content from JSON to your local type
What we would want to write is (1)
let getMyData () =
let token = Api.getToken()
let data = Api.getData token
return data
That is great and we all wished it was that simple. But what we forgot is that:
Api module
methods are hitting the REST API, thus they have to be asynchronous- Any
Api module
call can potentially fail for any reason (e.g. 401, 404, 500 http status) - Parsing the JSON may fail if the JSON is malformed
Based on this points, we can see that we are facing two side effects: latency and exception.
Latency is handled by transforming a function to an asynchronous function which returns an Async<'a>
result. Exception will be handled by a special type that we will define and call ApiResult<'a>
. It will either be a Success
or a Failure
.
By making side effects explicit, we end up with the following function definition:
//Started with
unit -> DataType
//Ended up with
unit -> Async<ApiResult<DataType>>
So what can we do with Async<ApiResult<'a>>
type? A lot! Thanks to computation expression in F#, we can manipulate the underlying type DataType
directly without thinking of Async
orApiResult
! In other words, we can write code without worrying about latency and exception. Doing this will allow us to write the code we saw in (1).
Making the computation expression
Just like the async
computation expression with async {...}
workflow, we want to define our own apiCall
computation expression which will allow us to use apiCall {...}
notation to write workflows which handle asynchronous calls and exceptions.
But where do we start? Here’s what we need to do:
- Define
ApiResult
- Make our
ApiCallBuilder
which is a type that allows us to instantiate the computation expression (’…Builder’ is just a convention, you could have it called ‘Hello’ and it would have worked the same way) - Instantiate the builder
apiCall
and use it!
So let’s start by defining what is an ApiResult
.
Defining ApiResult
ApiResult
will capture every possible outcomes of our Api calls and Json deserialization. Like what we saw earlier, the api may throw an error or the json may throw an error. So we define it like follows:
type ApiResult<'a> =
| Success of 'a
| Failure of ApiResponseException list
and ApiResponseException =
| Unauthorized of string
| NotFound of string
| UnsupportedMediaType of string
| BadRequest of string
| JsonDeserializeError of string
with
override this.ToString() =
match this with
| ApiResponseException.Unauthorized err -> err
| ApiResponseException.NotFound err -> err
| ApiResponseException.UnsupportedMediaType err -> err
| ApiResponseException.BadRequest err -> err
| ApiResponseException.JsonDeserializeError err -> err
This type specifies that ApiResult
is either a Success
or Failure
where the Failure
can be any of the followings: Unauthorized
, NotFound
, UnsupportedMediaType
, BadRequest
, JsonDeserializeError
.
Defining ApiCallBuilder
To build a computation expression, two methods are required Bind
and Return
(there are a bunch of them, you can go ahead and implement all if you want. It will give you more power in the workflow which is within the curly braces https://msdn.microsoft.com/en-us/library/dd233182.aspx). So we define it like that:
type ApiCallBuilder() =
member this.Bind(m, f) =
bind f m
member this.Return x =
retn x
Obviously, we don’t know what is bind
and retn
so let’s define it:
let retn x = async { return ApiResult.Success x }
// 'a -> Async<ApiResult<'a>>
let bind f m =
async {
let! xApiRes = m
match xApiRes with
| Success x -> return! f x
| Failure err -> return Failure err
}
//('a -> Async<ApiResult<'b>>) -> Async<ApiResult<'a>> -> Async<ApiResult<'b>>
Within the workflow:
let!
unwrapps the special type. Inlet! xApiRes = m
,m
is of typeAsync<ApiResult<'a>>
andxApiRes
is of typeApiResult<'a>
. See what we did there?let!
successfully transformedAsync<ApiResult<'a>>'
toApiResult<'a>
, it took awayAsync
!return
andreturn!
let you exit the workflow and return a result. The difference is thatreturn
lets you pass a underlying type'a'
and exits with a wrapped typeAsync<'a>'
whereasreturn!
lets you directly pass wrapped typeAsync<'a'>
and exits with the same wrapped typeAsync<'a'>
.
To define the builder we used retn
and bind
.
retn
takes a normal input of'a
and tranforms it to a special type ofAsync<ApiResult<'a>>
.bind
is used to transform a function that takes a normal input and returns a special type'a -> Async<ApiResult<'b>>
to a function that takes a special type and return a special typeAsync<ApiResult<'a>> -> Async<ApiResult<'b>>
.
Alright, we’ve done great so far. We are done with the ApiCallBuilder
. We can now instantiate it and use it as a keyword!
let apiCall = new ApiCallBuilder()
And we can use it like so:
let getMyData () =
apiCall {
let! token = Api.getToken() //token: string
let! data = Api.getData token //data: DataType
return data
}
//unit -> Async<ApiResult<DataType>>
Amazing! We are now manipulating simple types: string
and our ownDataType
instead of Async<ApiResult<string>>
and Async<ApiResult<DataType>>
. This is what we wanted to achieve at the beginning. Isn’t it wonderful? We used the computation expression to take-out latency
and exception
but you can now go ahead and create your own computation expressions which fit your scenarios to abstract side-effects like logging
or persistence
or anything else you like.
Where’s the WebSharper
At the start I said I will make a Ajax call from a SPA with WebSharper so let’s do it.
First I will define a general Ajax call method:
let ajaxCall reqType url contentTy headers data =
Async.FromContinuations
<| fun (ok, ko, _) ->
let settings = JQuery.AjaxSettings(
Url = "http://localhost/api/" + url,
Type = reqType,
DataType = JQuery.DataType.Text,
Success = (fun (result, _, _) ->
ok (result :?> string)),
Error = (fun (jqXHR, _, _) ->
ko (System.Exception(string jqXHR.Status)))
)
headers |> Option.iter (fun h -> settings.Headers <- h)
contentTy |> Option.iter (fun c -> settings.ContentType <- c)
data |> Option.iter (fun d -> settings.Data <- d)
JQuery.Ajax(settings) |> ignore
This method creates the AjaxSettings
which contains all the information about the http request and post it using JQuery.Ajax(settings)
. Async.FromContinuations
is used to turn a method which use Success
and Error
callbacks to a function which returns an Async
result.
We also define utility methods to convert http code to Failure
and to deserialize Json string to ApiResult
.
let private matchErrorStatusCode url code =
match code with
| "401" -> Failure [ ApiResponseException.Unauthorized
<| sprintf """"%s" - 401 The Authorization header did not pass security""" url]
| "404" -> Failure [ ApiResponseException.NotFound
<| sprintf """"%s" - 404 Endpoint not found""" url ]
| "415" -> Failure [ ApiResponseException.UnsupportedMediaType
<| sprintf """"%s" - 415 The request Content-Type is not supported/invalid""" url]
| code -> Failure [ ApiResponseException.BadRequest
<| sprintf """"%s" - %s Bad request""" url code ]
let tryDeserialize deserialization input =
match input with
| Success json ->
try
deserialization json |> ApiResult.Success
with _ -> Failure [ ApiResponseException.JsonDeserializeError
<| sprintf """"{%s}" cannot be deserialized""" json ]
| Failure err -> Failure err
|> Async.retn
//(string -> 'a) -> ApiResult<string> -> Async<ApiResult<'a>>
And we define two new types to handle the authentication and token.
type AuthToken = AuthToken of string
type Credentials = {
UserName:string
Password:string
} with
static member Default = { UserName = "admin"; Password = "admin" }
We use the type AuthToken
to represent the token. It allows us to type the argument of methods expecting a string token and take an AuthToken
instead. Credentials
is a record type which holds the user name and password.
Great now that we have our foundation for Ajax calls, let’s make the first method getToken
.
let getToken () =
let url = "auth/login/token"
async {
try
let! token = ajaxCall JQuery.RequestType.POST url (Some "application/json") None (Some (Json.Serialize(Credentials.Default)))
return ApiResult.Success token
with ex -> return matchErrorStatusCode url ex.Message
} |> Async.bind (tryDeserialize (Json.Deserialize<string> >> AuthToken))
//unit -> Async<ApiResult<AuthToken>>
The Ajax call returns a Async<string>
but it can throw exception so we want to make the exception explicit in the type. In order to do that, we try catch
the Ajax call and return a Async<ApiResult<string>>
which will take in account the exception. If you remember, an ApiResult
is either a Success
or a Failure
and in the case of a Failure
, it can be Unauthorized
or NotFound
etc…
After that we pipe it to the deserialization with tryDeserialize
which in case of error, returns a Failure of JsonDeserializeError
.
We do the same for getData
.
let getData (AuthToken token) =
let url = "data"
async {
try
let! data = ajaxCall JQuery.RequestType.GET url None (Some (Object<string> [|("Authorization", "Bearer " + token)|])) None
return ApiResult.Success (data)
with ex -> return matchErrorStatusCode url ex.Message
} |> Async.bind (tryDeserialize Json.Deserialize<MyData>)
Finally we can use getToken
and getData
in a apiCall
workflow and bind it to a button in our HTML template.
type IndexTemplate = Template<"index.html">
let Main =
JQuery.Of("#main").Empty().Ignore
let rvData = Var.Create ""
let getData = apiCall {
let! token = ApiClient.getToken ()
let! data = ApiClient.getData token
return data
}
let GetDataBtn =
div [ Doc.Button "Get data" []
<| fun _ -> async {
let! data = getData
do match data with
| Success data -> sprintf "%A" data
|> Var.Set rvData
| Failure exs -> exs |> List.map string
|> String.concat "-"
|> JS.Alert
} |> Async.Start ]
IndexTemplate.Main.Doc(Action = [ GetDataBtn ], Content = rvData.View)
|> Doc.RunById "main"
In getData
we see how to use our apiCall
keyword to write nice code to get the token and then use it to get the data.
And we are done! What we have built is a complete recipe to interact with our REST Api from WebSharper front-end. The code looks simple now (I think…) but it handles asynchronous calls and handles exception in the background!
If we need to make other calls to the Api, we can just write it inside an apiCall {...}
block and we get async and error handling for free!
And here is the HTML.
<div id="main" data-children-template="Main">
<div class="row">
<div class="large-6 columns" data-hole="Action"></div>
<div class="large-6 columns">
$!{Content}
</div>
</div>
</div>
Conclusion
Today we explored how to create a computation expression in F# and how we could use it to write simple code which would abstract latency and exception. We also learnt how to query a REST Api from a WebSharper SPA using WebSharper.JQuery and how to deserialize the result using WebSharper.Json. I hope you enjoyed this post as much as I enjoyed writting it. I am by no mean an expert, in fact I just started to learn about F# two months ago and WebSharper a month ago and I love it! So if you find any errors or if you feel that this is not the right way, please share with me your remarks. Thanks for reading!
Comments
Post a Comment