神刀安全网

Implementing API Gateway in F# using Rx and Suave

One of the impressive aspects of functional programming is that it will enable you to write simple and expressive code to solve complex problems. If you are wondering, how is it possible? Well, It is because, Functional Programming provides a lot of higher level abstractions to solve the problem in hand and avoids the need of writing lot of boilerplate & plumbing code. As a developer, you will be more productive and deliver the solution faster.

In this blog post, we are going to see the application of the above statements by implementing an API Gateway Pattern in F# using Rx and Suave .

This blog post is my contribution towards fsharp advent calendar 2015 . Do check out other great fsharp posts there

API Gateway Pattern

Let’s assume that you are asked to build the backend for the below GitHub user profile screen.

Implementing API Gateway in F# using Rx and Suave

This profile screen has 3 components

  1. User – Username and Avatar

  2. Popular Repos – Top 3 Repos of the Given User (determined by the number of stars)

  3. Languages being used in the corresponding repos

Now as a developer, you have two options

  • Build the Profile completely in client side by calling their corresponding GitHub APIs Implementing API Gateway in F# using Rx and Suave

    This approach makes the client side complex as it involves five HTTP requests to create the complete profile screen. It has a lot of drawbacks as mentioned in this blog post by Netflix.

  • Create a Facade API which takes care of the heavy lifting and hides the complexity of calling multiple services by exposing a single endpoint which returns all the data required to create the profile screen in one API call Implementing API Gateway in F# using Rx and Suave

    This approach is called API Gateway. It is one of the commonly used patterns in the microservices world.

Rx

API Gateway is normally implemented using Rx . Rx, an implementation of Functional Reactive Programming , makes it easy to implement this kind of problems by enable us to think each asynchronous operation as streams (aka Observables). These streams offer a declarative API to work with the asynchronous operations.

In the implementation that we are going to see, we will be using FSharp.Control.Reactive , an extension and wrapper for using the Reactive Extensions (Rx) with F#

Project Structure

Create a new F# Console Application Project with the name RxFsharp and then create the files mentioned below in the given order

RxFsharp |--Http.fs --> Abstraction to make http requests |--GitHub.fs --> Define data types, transforming & parsing of responses from GitHub |--ObservableExtensions.fs --> Some Util methods over Rx Observable |--Profile.fs --> Implemention of API Gateway using Rx |--ApiGateway.fs --> Suave API |--Program.fs --> Entry Point |--user.json --> Sample response of GitHub user API |--repos.json --> Sample response of GitHub repos API |--App.config |--packages.config

Then install the following NuGet packages

Modeling Http Request & Response as Stream

In Functional Reactive Programming(FRP) every action is treated as stream. So, first step in implementing an API Gateway is modeling each HTTP request&response as stream.

Open Http.fs file and update it as below

module Http  open HttpClient open FSharp.Control.Reactive  type HttpResponse = | Ok of string | Error of int  let getResponseAsync url =   async {     let! response =       createRequest Get url       |> withHeader (UserAgent "FsharpRx")       |> HttpClient.getResponseAsync     let httpResponse =       match response.StatusCode with       | 200 -> response.EntityBody.Value |> Ok       | _ -> response.StatusCode |> Error     return httpResponse   }  let asyncResponseToObservable = getResponseAsync >> Observable.ofAsync 

The getResponseAsync function makes use of Http.fs , an HTTP Client library for F#, to fire the HTTP GET request and returns the HTTP response asynchronously.

The HTTP response has been modeled as either Ok with the response content for all the responses with the status code 200 and everything else is treated as Error for simplicity.

In the final line, we create a stream (aka Observable) from asynchronous response using the function Observable.ofAsync from FSharp.Control.Reactive

As all the GitHub API requests require user agent , we are adding one before firing the request.

Parsing and Transforming GitHub API Responses

Upon successful response from GitHub, the next step is parsing the JSON response and transforming the parsed response as per the Profile screen requirements.

To parse the response, we are going to leverage the powerful tool of FSharp called JsonTypeProvider

The JSON Type Provider provides statically typed access to JSON documents. It takes a sample document as an input. Here in our case, it is user.json and repos.json

Get the sample response from GitHub using its User API and Repos API and save the response in user.json and repos.json respectively.

With the help of JsonTypeProvider, we can now easily parse the raw JSON response to its equivalent types in just a few lines of code in GitHub.fs !

module GitHub  open Http open FSharp.Data  type GitHubUser = JsonProvider<"user.json"> type GitHubUserRepos = JsonProvider<"repos.json">  let parseUser = GitHubUser.Parse let parseUserRepos = GitHubUserRepos.Parse 

As GitHub.fs is an abstraction of GitHub let’s put their URLs also here in the same file

let host = "https://api.github.com" let userUrl = sprintf "%s/users/%s" host let reposUrl = sprintf "%s/users/%s/repos" host let languagesUrl repoName userName = sprintf "%s/repos/%s/%s/languages" host userName repoName 

We have used partial application of function sprintf in the userUrl and reposUrl function here, so both of these functions take username as its parameter implicitly

The JSON response of languages API is little tricky to parse as its keys are dynamic. The type provider will work only if the underlying response has a fixed schema. So, we can’t use the type provider directly to parse the response of languages API.

We are going to use the inbuilt JSON Parser available with JsonTypeProvider to parse the languages response

let parseLanguages languagesJson =   languagesJson   |> JsonValue.Parse   |> JsonExtensions.Properties   |> Array.map fst 

The JsonValue.Parse parses the raw string JSON to a type called JsonValue and the JsonExtensions.Properties function takes a JsonValue and returns the key and value pairs of all the properties in the JSON as tuples. As we are interested only in the Keys here, we just pluck that value alone using the function fst

Great! Now we are done with parsing the JSON response of all the three APIs and creating it’s equivalent types. The next step is doing some business transformation

One of the requirements of the Profile Screen is that we need to show only the top 3 popular repositories based on the stars received by the repository. Let’s implement it

let popularRepos (repos : GitHubUserRepos.Root []) =   let ownRepos = repos |> Array.filter (fun repo -> not repo.Fork)   let takeCount = if ownRepos.Length > 3 then 3 else repos.Length    ownRepos   |> Array.sortBy (fun r -> -r.StargazersCount)   |> Array.toSeq   |> Seq.take takeCount   |> Seq.toArray 

In the Http.fs file, we defined how to get the raw JSON response using Stream as HttpResponse ( Ok & Error ) and in this GitHub.fs , we have defined how to parse this raw JSON response and transform them its equivalent strongly types.

The final step is integrating both these logic and return the Output record type needed by the Profile Screen

let’s start by defining the Profile type

type Profile = {   Name : string   AvatarUrl : string   PopularRepositories : Repository seq } and Repository = {   Name : string   Stars : int   Languages : string[] } 

Then write functions to get the value from HttpResponse and create this profile output.

let reposResponseToPopularRepos = function   |Ok(r) -> r |> parseUserRepos |> popularRepos   |_ -> [||]  // Languages always associated with a repository let languageResponseToRepoWithLanguages (repo : GitHubUserRepos.Root) = function   |Ok(l) -> {Name = repo.Name; Languages = (parseLanguages l); Stars = repo.StargazersCount}   |_ -> {Name = repo.Name; Languages = Array.empty; Stars = repo.StargazersCount}  let toProfile  = function   |Ok(u), repos ->       let user = parseUser u       {Name = user.Name; PopularRepositories = repos; AvatarUrl = user.AvatarUrl} |> Some   | _ -> None 

All the above functions except toProfile take HttpResponse as its last parameter implicitly.

In reposResponseToPopularRepos function, if the repos API request is successful, we parse the response to its equivalent type and then pick only three of them based on star count and in the case of an error we just return an empty array.

The languageResponseToRepoWithLanguages function handles the response from the Languages API request which takes its associated repository as its first parameter. If the response is successful, then it creates the Repository record with the returned languages else it just the Repository record with an empty array for languages.

The last function toProfile is a merge function which takes a tuple of HttpResponse (of User API request) and Repository [] and creates a Profile record if the response is successful. In case of an error, it just returns None

Note: To keep this blog post simple, I am handling the errors using empty arrays and None. It can be extended using ROP

Implementing API Gateway

Let me quickly summarize what we have done so far. We have created two abstractions.

  • Http – Responsible for firing the HTTP GET request with the given URL and give the response as a Rx Stream

URL   / ----------Response--| 

  • GitHub – Takes care of parsing the JSON response from GitHub API and does some business logic (finding top 3 popular repositories). Then returns the output in the format that the Client needs.

With the help of these abstractions now we are going to build the API Gateway to get the profile object.

Open Gateway.fs and add the getProfile function

//string -> Async<Profile> let getProfile username =   async {     // TODO     return! profile   } 

It is just a skeleton which just describes the what function does. Let’s start its implementation.

In Rx world, everything is a stream. So, the first step is converting GitHub User API request to stream

module Http  open HttpClient open FSharp.Control.Reactive  type HttpResponse = | Ok of string | Error of int  let getResponseAsync url =   async {     let! response =       createRequest Get url       |> withHeader (UserAgent "FsharpRx")       |> HttpClient.getResponseAsync     let httpResponse =       match response.StatusCode with       | 200 -> response.EntityBody.Value |> Ok       | _ -> response.StatusCode |> Error     return httpResponse   }  let asyncResponseToObservable = getResponseAsync >> Observable.ofAsync 

0

We have created the userStream using the userUrl and asyncResponseToObservable function defined earlier.

module Http  open HttpClient open FSharp.Control.Reactive  type HttpResponse = | Ok of string | Error of int  let getResponseAsync url =   async {     let! response =       createRequest Get url       |> withHeader (UserAgent "FsharpRx")       |> HttpClient.getResponseAsync     let httpResponse =       match response.StatusCode with       | 200 -> response.EntityBody.Value |> Ok       | _ -> response.StatusCode |> Error     return httpResponse   }  let asyncResponseToObservable = getResponseAsync >> Observable.ofAsync 

1

Then we need top three popular repositories as a stream

module Http  open HttpClient open FSharp.Control.Reactive  type HttpResponse = | Ok of string | Error of int  let getResponseAsync url =   async {     let! response =       createRequest Get url       |> withHeader (UserAgent "FsharpRx")       |> HttpClient.getResponseAsync     let httpResponse =       match response.StatusCode with       | 200 -> response.EntityBody.Value |> Ok       | _ -> response.StatusCode |> Error     return httpResponse   }  let asyncResponseToObservable = getResponseAsync >> Observable.ofAsync 

2

Except the last line, everything is same as that of creating userStream . If you check GitHub repos API it just returns all the repos associated with the given username. But what we need is only top three of them based on the number of stars it has received.

We already have a function reposResponseToPopularRepos in GitHub.fs which does the job of picking the top three repos from the raw JSON. As the response is in the form Rx stream, we need to get the value from the stream and then we need to apply this function and that’s what the Observable.map does. It is very similar to Array.map and LINQ Select

module Http  open HttpClient open FSharp.Control.Reactive  type HttpResponse = | Ok of string | Error of int  let getResponseAsync url =   async {     let! response =       createRequest Get url       |> withHeader (UserAgent "FsharpRx")       |> HttpClient.getResponseAsync     let httpResponse =       match response.StatusCode with       | 200 -> response.EntityBody.Value |> Ok       | _ -> response.StatusCode |> Error     return httpResponse   }  let asyncResponseToObservable = getResponseAsync >> Observable.ofAsync 

3

The next operation is a little complex. i.e. finding out the programming languages being used in these popular repositories. To get this GitHub API, we need to fire three separate requests to the Languages API for each repository and then merge the results back with the corresponding repository.

To implement this with the help of Rx streams, we need to understand the flatMap function of Rx .

It is similar to the map function that we have seen before with a difference that it takes a function that returns a new item stream instead of a new item.

Map

Implementing API Gateway in F# using Rx and Suave

The map function has the following signature

module Http  open HttpClient open FSharp.Control.Reactive  type HttpResponse = | Ok of string | Error of int  let getResponseAsync url =   async {     let! response =       createRequest Get url       |> withHeader (UserAgent "FsharpRx")       |> HttpClient.getResponseAsync     let httpResponse =       match response.StatusCode with       | 200 -> response.EntityBody.Value |> Ok       | _ -> response.StatusCode |> Error     return httpResponse   }  let asyncResponseToObservable = getResponseAsync >> Observable.ofAsync 

4

FlatMap

Implementing API Gateway in F# using Rx and Suave

The flatMap function has the following signature

module Http  open HttpClient open FSharp.Control.Reactive  type HttpResponse = | Ok of string | Error of int  let getResponseAsync url =   async {     let! response =       createRequest Get url       |> withHeader (UserAgent "FsharpRx")       |> HttpClient.getResponseAsync     let httpResponse =       match response.StatusCode with       | 200 -> response.EntityBody.Value |> Ok       | _ -> response.StatusCode |> Error     return httpResponse   }  let asyncResponseToObservable = getResponseAsync >> Observable.ofAsync 

5

This function is also called as bind function in the functional programming world. If you would like to know further on this topic, I strongly recommend this blog series by the fsharp great, Scott Wlaschin

Back to the problem in hand, we need to fire three HTTP GET Requests to get back the languages associated with the each of the top three popular repos. In terms of the flatMap function, it boils down to three functions of the following syntax

module Http  open HttpClient open FSharp.Control.Reactive  type HttpResponse = | Ok of string | Error of int  let getResponseAsync url =   async {     let! response =       createRequest Get url       |> withHeader (UserAgent "FsharpRx")       |> HttpClient.getResponseAsync     let httpResponse =       match response.StatusCode with       | 200 -> response.EntityBody.Value |> Ok       | _ -> response.StatusCode |> Error     return httpResponse   }  let asyncResponseToObservable = getResponseAsync >> Observable.ofAsync 

6

We can implement the solution using three flatMap functions using the above syntax. But, we can make it more granular by creating a new variant of flatMap function to achieve this in a more straightforward way.

The ideal function that we are looking for in this flatMap variant holds the following signature

module Http  open HttpClient open FSharp.Control.Reactive  type HttpResponse = | Ok of string | Error of int  let getResponseAsync url =   async {     let! response =       createRequest Get url       |> withHeader (UserAgent "FsharpRx")       |> HttpClient.getResponseAsync     let httpResponse =       match response.StatusCode with       | 200 -> response.EntityBody.Value |> Ok       | _ -> response.StatusCode |> Error     return httpResponse   }  let asyncResponseToObservable = getResponseAsync >> Observable.ofAsync 

7

i.e three flatMap can be rewritten as

module Http  open HttpClient open FSharp.Control.Reactive  type HttpResponse = | Ok of string | Error of int  let getResponseAsync url =   async {     let! response =       createRequest Get url       |> withHeader (UserAgent "FsharpRx")       |> HttpClient.getResponseAsync     let httpResponse =       match response.StatusCode with       | 200 -> response.EntityBody.Value |> Ok       | _ -> response.StatusCode |> Error     return httpResponse   }  let asyncResponseToObservable = getResponseAsync >> Observable.ofAsync 

8

Let’s name it as flatMap2 and add the implementation of it in the file ObservableExtensions.fs

module Http  open HttpClient open FSharp.Control.Reactive  type HttpResponse = | Ok of string | Error of int  let getResponseAsync url =   async {     let! response =       createRequest Get url       |> withHeader (UserAgent "FsharpRx")       |> HttpClient.getResponseAsync     let httpResponse =       match response.StatusCode with       | 200 -> response.EntityBody.Value |> Ok       | _ -> response.StatusCode |> Error     return httpResponse   }  let asyncResponseToObservable = getResponseAsync >> Observable.ofAsync 

9

Here is the representation of what this function does

module GitHub  open Http open FSharp.Data  type GitHubUser = JsonProvider<"user.json"> type GitHubUserRepos = JsonProvider<"repos.json">  let parseUser = GitHubUser.Parse let parseUserRepos = GitHubUserRepos.Parse 

0

It’s hard to get it right in a single shot. So, let’s see it in detail step by step by applying it to our use case here

  • We have the stream of three popular repos (i.e array of repos)

module GitHub  open Http open FSharp.Data  type GitHubUser = JsonProvider<"user.json"> type GitHubUserRepos = JsonProvider<"repos.json">  let parseUser = GitHubUser.Parse let parseUserRepos = GitHubUserRepos.Parse 

1

  • To get the languages associated with the each repo we need to call the languages API for every repo item in the above step

module GitHub  open Http open FSharp.Data  type GitHubUser = JsonProvider<"user.json"> type GitHubUserRepos = JsonProvider<"repos.json">  let parseUser = GitHubUser.Parse let parseUserRepos = GitHubUserRepos.Parse 

2

  • After we received the response from each languages API call, we need to merge them into one stream

module GitHub  open Http open FSharp.Data  type GitHubUser = JsonProvider<"user.json"> type GitHubUserRepos = JsonProvider<"repos.json">  let parseUser = GitHubUser.Parse let parseUserRepos = GitHubUserRepos.Parse 

3

  • To integrate this with the expected application response, we need all the responses in a single go

module GitHub  open Http open FSharp.Data  type GitHubUser = JsonProvider<"user.json"> type GitHubUserRepos = JsonProvider<"repos.json">  let parseUser = GitHubUser.Parse let parseUserRepos = GitHubUserRepos.Parse 

4

Great! You got it right!!

Let’s use this flatMap2 function and complete the implementation of profile API gateway.

module GitHub  open Http open FSharp.Data  type GitHubUser = JsonProvider<"user.json"> type GitHubUserRepos = JsonProvider<"repos.json">  let parseUser = GitHubUser.Parse let parseUserRepos = GitHubUserRepos.Parse 

5

The flatmap2 function takes the function toRepoWithLanguagesStream which converts the GitHubUserRepos.Root type to IObservable<Repository> to find out the languages associated with the given popular repositories.

The toRepoWithLanguagesStream function does the following

module GitHub  open Http open FSharp.Data  type GitHubUser = JsonProvider<"user.json"> type GitHubUserRepos = JsonProvider<"repos.json">  let parseUser = GitHubUser.Parse let parseUserRepos = GitHubUserRepos.Parse 

6

The Observable.map function takes only one input, but here we need to two inputs. So, With the help of partial application , we created an intermediate function by partially applying the first parameter alone

module GitHub  open Http open FSharp.Data  type GitHubUser = JsonProvider<"user.json"> type GitHubUserRepos = JsonProvider<"repos.json">  let parseUser = GitHubUser.Parse let parseUserRepos = GitHubUserRepos.Parse 

7

The languageResponseToRepoWithLanguages function has been already defined in the GitHub.fs file.

The last step of the getProfile function is combining this popularReposStream with the userStream created earlier and return the Profile type asynchronously.

module GitHub  open Http open FSharp.Data  type GitHubUser = JsonProvider<"user.json"> type GitHubUserRepos = JsonProvider<"repos.json">  let parseUser = GitHubUser.Parse let parseUserRepos = GitHubUserRepos.Parse 

8

The Observable.zip function takes two streams as its input, then merges the output of each stream and return the output as a tuple. From this tuple, we have used Observable.map function to map it a Profile type using the toProfile function created earlier in GitHub.fs

module GitHub  open Http open FSharp.Data  type GitHubUser = JsonProvider<"user.json"> type GitHubUserRepos = JsonProvider<"repos.json">  let parseUser = GitHubUser.Parse let parseUserRepos = GitHubUserRepos.Parse 

9

The last functions TaskObservableExtensions.ToTask and Async.AwaitTask does the conversion of IObservable to async by converting it to a Task first and then the Task to async

The final getProfile function will be like

let host = "https://api.github.com" let userUrl = sprintf "%s/users/%s" host let reposUrl = sprintf "%s/users/%s/repos" host let languagesUrl repoName userName = sprintf "%s/repos/%s/%s/languages" host userName repoName 

0

This function is a testimonial on How functional programming helps to write less, robust, and readable code to solve a complex problem

We have handled all the five HTTP requests asynchronously, did some error handling (by returning empty types), and finally efficiently combined outputs of these five HTTP requests and created a type to send back to the client. Everything is asynchronous!

Pretty awesome isn’t it?

Exposing the API

The final step is exposing what we have done so far as an API to the outside world. We are going to implement this using Suave

Open ApiGateway.fs file and update it as below

let host = "https://api.github.com" let userUrl = sprintf "%s/users/%s" host let reposUrl = sprintf "%s/users/%s/repos" host let languagesUrl repoName userName = sprintf "%s/repos/%s/%s/languages" host userName repoName 

1

The JSON is a utility function (WebPart in the world of Suave) which takes any type, serialize it to JSON format and return it as JSON HTTP response.

The getProfile function is the API WebPart which calls our backend API gateway implementation and pass the received response to the JSON WebPart defined before.

In case if there is no profile available (Remember? we return empty types in case of errors), we just return 404 with the message that the given username is not found.

Then update the Program.fs to write the web server code

let host = "https://api.github.com" let userUrl = sprintf "%s/users/%s" host let reposUrl = sprintf "%s/users/%s/repos" host let languagesUrl repoName userName = sprintf "%s/repos/%s/%s/languages" host userName repoName 

2

Thanks to Suave for it’s lightweight and low-ceremony offerings in creating an API. We just exposed it in two lines!

Hit F5 and access the API at http://localhost:8083/api/profile/{github-username} Bingo!!

Summary

A language that doesn’t affect the way you think about programming is not worth knowing – Alan Perils

The above quote summarizes the gist of this blog post. Functional Programming will help you think better. You can get the source code associated with the blog post in my blog-samples GitHub repository

Wish you a happy and prosperous new year

原文  http://blog.tamizhvendan.in/blog/2015/12/29/implementing-api-gateway-in-f-number-using-rx-and-suave/

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Implementing API Gateway in F# using Rx and Suave

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
分享按钮