nfrankel commented on code in PR #1285: URL: https://github.com/apache/apisix-website/pull/1285#discussion_r948812274
########## blog/en/blog/2022/08/17/backend-for-frontend-demo.md: ########## @@ -0,0 +1,299 @@ +--- +title: "Backend-for-Frontend: the demo" +authors: + - name: Nicolas Fränkel + title: Author + url: https://github.com/nfrankel + image_url: https://avatars.githubusercontent.com/u/752258 +keywords: +- API gateway +- Apache APISIX +- System architecture +- Mciroservices +description: This article describes a demo code to implement the Backend-For-Frontend pattern. +tags: [Case Studies] +--- + +> This article describes a demo code to implement the Backend-For-Frontend pattern. + +<!--truncate--> + +<head> + <link rel="canonical" href="https://blog.frankel.ch/backend-for-frontend-demo/" /> +</head> + +In [one of my earlier posts](https://blog.frankel.ch/backend-for-frontend/), I described the Backend-for-Frontend pattern. In short, it offers a single facade over multiple backend parts. Moreover, it provides each client type, _e.g._ desktop, mobile, exactly the data that it needs and not more in the format required by this client type. + +## The use-case + +Imagine the following use-case. In a e-commerce shop, the home page should display multiple *unrelated* data at once. + +* Products: The business could configure which items are shown on the home page. They could be generic, "hero" products, or personalized, products that the customer ordered previously. +* News: Again, the newsfeed could be generic or personalized. +* Profile-related information +* Cart content +* Non-business related information, such as build number, build timestamp, version, etc. + +Depending on the client, we want more or less data. For example, on a client with limited display size, we probably want to limit a product to its name and its image. On the other hand, on desktop, we are happy to display both the above, plus a catch phrase (or a more catchy - and longer - name) and the full description. + +Every client requires its specific data and for performance reasons, we want to fetch them in a single call. It sounds like a use-case for <abbr title="Backend-for-Frontend">BFF</abbr>. + +## Setting up the demo + +In order to simplify things, I'll keep only three sources of data: products, news and technical data. Three unrelated data sources are enough to highlight the issue. + +In the demo, I'm using Python and Flask, but the underlying technology is irrelevant, since BFF is an architectural pattern. + +The initial situation is a monolith. The monolith offers an endpoint for each data source, and a single aggregating endpoint for all of them: + +```python +@app.route("/") +def home(): + return { + 'products': products, #1 + 'news': news, #1 + 'info': debug #1 + } +``` + +1. Somehow get the data internally, _e.g._, from the database + +At this point, everything is fine. We can provide different data depending on the client: + +* If we want to put the responsibility on the client, we provide a dedicated endpoint +* If we want it server-side, we can read the `User-Agent` from the request (or agree on a specific `X-` HTTP header) + +As it doesn't add anything to the demo, I won't provide different data depending on the client in the following. + +## Migrating to microservices + +At one point, the organization decides to migrate to a microservices architecture. The reason might be because the CTO read about microservices in a blog post, because the team lead wants to add microservices on its resume, or even because the development grew too big and the organization do need to evolve. In any case, the monolith has to be split in *two* microservices: a catalog providing products and a newsfeed providing... news. + +Here's the code for each microservice: + +```python +@app.route("/info") +def info(): + return debug #1 + + +@app.route("/products") +def get_products(): + return jsonify(products) #2 +``` + +1. Each microservice has its own `debug` endpoint +2. The payload is not an object anymore but an array + +```python +@app.route("/info") +def info(): + return debug #1 + + +@app.route("/news") +def get_news(): + return jsonify(news) #1 +``` + +1. As above + +Now, each client needs two calls, and filter out data that are not relevant. + +## Dedicated backend-for-frontend + +Because of the issues highlighted above, a solution is to develop one application that does the aggregation and filtering. There should be one for each client type, and it should be cared for by the same team as the client. Again, for this demo, it's enough to have a single one that only does aggregation. + +```python +@app.route("/") +def home(): + products = requests.get(products_uri).json() #1 + catalog_info = requests.get(catalog_info_uri).json() #2 + news = requests.get(news_uri).json() #1 + news_info = requests.get(news_info_uri).json() #2 + return { + 'products': products, + 'news': news, + 'info': { #3 + 'catalog': catalog_info, + 'news': news_info + } + } +``` + +1. Get data +2. Get debug info +3. The returned JSON should be designed for easy consumption on the client side. To illustrate it, I chose for the debug data to be nested instead of top-level. + +## Backend-for-frontend at the API Gateway level + +If you're offering APIs, whether internally or to the outside world, chances are high that you're already using an API Gateway. If not, you should probably [deeply consider](https://apisix.apache.org/docs/apisix/terminology/api-gateway/) starting to. In the following, I assume that you do use one. + +In the previous section, we developed a dedicated backend-for-frontend application. However, requests already go through the gateway. In this case, the gateway can be seen as a container where to deploy BFF plugins. I'll be using [Apache APISIX](https://apisix.apache.org/) to demo how to do it, but the idea can be replicated on other gateways as well. + +First things first, there's no generic way to achieve the result we want. For this reason, we cannot rely on an existing plugin, but we have to design our own. APISIX documents [how to do so](https://apisix.apache.org/docs/apisix/plugin-develop/). Our goal is to fetch data from all endpoints as above, but via the plugin. + +First, we need to expose a dedicated endpoint, _e.g._, `/bff/desktop` or `/bff/phone`. APISIX allows such _virtual_ endpoints via the [public-api](https://apisix.apache.org/docs/apisix/plugins/public-api/) plugin. Next, we need to develop our plugin, `bff`. Here's the configuration snippet: + +```yaml +routes: + - uri: / #1 + plugins: + bff: ~ #2 + public-api: ~ #2 +``` + +1. For demo purposes, I preferred to set it at the root instead of `/bff/*` +2. Declare the two plugins. Note that I'm using the [stand-alone mode](https://apisix.apache.org/docs/apisix/stand-alone/). + +First, we need to describe the plugin and not forget to return it at the end of the file: + +```lua +local plugin_name = 'bff-plugin' + +local _M = { --1 + version = 1.0, + priority = 100, --2 + name = plugin_name, + schema = {}, --3 +} + +return _M --4 +``` + +1. The table needs to be named `_M` +2. In this scenario, `priority` is irrelevant as no other plugins are involved (but `public_api`) +3. No schema is necessary as there's no configuration +4. Don't forget to return it! + +A plugin that has a public API needs to define an `api()` function returning an object describing matching HTTP methods, the matching URI, and the handler function. + +```lua +function _M.api() + return { + { + methods = { 'GET' }, + uri = "/", + handler = fetch_all_data, + } + } +end +``` + +Now, we have to define the `fetch_all_data` function. It's only a matter of making HTTP calls to the catalog and newsfeed microservices. Have a look at [the code](https://github.com/ajavageek/backend-for-frontend/blob/master/bff_plugin/init.lua#L24-L52) if you're interested in the exact details. + +At this point, the (single) client can query `http://localhost:9080/` and get the complete payload. + +In a "real life" microservices-based organization, every team should be independent of each other. With this approach, each can develop its own BFF as a plugin and deploy it independently in the gateway. + +## Bonus: a poor man's BFF + +The microservices architecture creates two problems for clients: + +1. The need to fetch all data and filter out the unnecessary ones +2. Multiple calls to each service + +The BFF pattern allows to fix both of them, at the cost of custom development, regardless whether it's for a dedicated app or a gateway plugin. If you're not willing to spend time in custom development, you can still avoid #2 by using a nifty plugin of Apache APISIX, `batch-requests`: + +>The `batch-requests` plugin accepts multiple requests, sends them from APISIX via HTTP pipelining, and returns an aggregated response to the client. +> +>This improves the performance significantly in cases where the client needs to access multiple APIs. +> +> -- [batch-requests](https://apisix.apache.org/docs/apisix/plugins/batch-requests/) Review Comment: Actually, it's not a note but a direct copy-paste from the documentation. Hence, it makes sense to use a quote. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: notifications-unsubscr...@apisix.apache.org For queries about this service, please contact Infrastructure at: us...@infra.apache.org