Blog
/
Education

5 Best Practices for Backends-for-Frontends

cover

Prithwish Nath

min read

We're hiring!

We're looking for Golang (Go) Developers, DevOps Engineers and Solution Architects who want to help us shape the future of Microservices, distributed systems, and APIs.

By working at WunderGraph, you'll have the opportunity to build the next generation of API and Microservices infrastructure. Our customer base ranges from small startups to well-known enterprises, allowing you to not just have an impact at scale, but also to build a network of industry professionals.

5 Best Practices for Backends-for-Frontends

Best practices to follow when implementing the BFF pattern: do’s and don’ts.

Photo by Arnold Francisca on Unsplash

The Backends-for-Frontends (BFF) pattern is an interesting solution to a problem many teams face — meaningfully decoupling the frontend from the backend, insulating the former from changes to the latter.

In this article, I’m going to quickly explain the BFF pattern, and then focus on some of the best practices you should be following when implementing it. If you’d like to know more about the pattern itself, I’d recommend reading this interesting article over here .

So let’s get going!

What is the BFF pattern?

BFF stands for Backend For Frontend, and it’s a very clever way to architect your application in a way so that no matter how many different frontend experiences you offer, the core backend services remain intact.

What does this do differently? Well, sometimes you might be offering very distinct frontend experiences — multiple apps that are meant to re-use your backend services, while each offering very different UXs to your users.

Think of a limited mobile UX, vs. a feature-rich desktop app, vs. a reporting SMS interface. These are three very different UI/UX, all consuming the same business logic at their core. But if you try to create a unique and generic set of microservices that can cater to all of those UI’s needs, you run the risk of introducing unnecessary coupling between the frontend and backend. This always leads to bloated and difficult-to-maintain codebases, and business logic leaking into the frontend.

And we all know, that’s not ideal. Usually, you’ll want your client apps to be “dumb” keeping all the “smarts” on the backend.

And how do we solve this?

On the right, you’ll see the BFF pattern — creating a second layer of “backend”, one that is specifically built for each UI/UX, that acts as a middleman between the “dumb” UI and the “smart” shared core backend services.

The diagram on the left shows a scenario where you had to make your client apps bigger, bloated, holding some of the business logic required to make it all work.

On the right side though, with a BFF, you can see how the implementation of a new “backend “ layer per UX helps to simplify the client applications, breaking the tight coupling between the frontend and the backend services (whether internal or external APIs), allowing you to update and change them however you see fit, while keeping the client-side code intact (you simply absorb the changes in the BFFs, accordingly).

Now that you have a working understanding of the BFF pattern, let’s take a look at some of the best practices around this interesting pattern.

1. Create one BFF per User Experience

The primary role of a BFF should be to handle the needs of its specific frontend client — and no more.

Look at the above diagram, the microservices are still there, they’re the ones that own the “core business logic”. The extra layer added by the BFF is only there to help solve problems that are specific to each UX.

What do I mean by that? In the above example, I proposed 3 different clients for our APIs:

  • SMS
  • Mobile
  • Desktop app

Each of them provides a unique user experience — but what’s critical here is that we’re talking about serving specific needs based on the UI/UX provided, not the specific needs of each separate client implementation.

Even if, for example, there were 2 mobile apps (one for Android and one for iOS), they would still provide (more or less) the same UI/UX, and have similar data needs, therefore you’d only need one “mobile experience” BFF, not one BFF for Android, and one for iOS.

Doing it on a per-client basis would only lead to code duplication for no good reason. There is a significant difference between “the needs of a specific UI/UX” and “the needs of a specific client” that we need to remember when architecting our BFFs.

2. Do not reinvent the wheel

Implementing the BFF pattern can be as hard and difficult or as simple and trivial as you want.

My suggestion would be to try and find a framework that gives you the ability to easily implement this pattern without you having to do much.

In the end, what you want is a layer of abstraction over your microservices. One such framework is WunderGraph .

WunderGraph is a “BFF framework” that allows you to bring together all of your underlying services, external APIs, or data sources, and create a single, unified API out of them, thus decoupling your clients from your downstream services.

The following example shows how easy it is to create an app that composes two different external APIs and lets your frontend devs use them without calling them directly.

First. Let’s create a sample NextJS application with WunderGraph included:

npx create-wundergraph-app my-weather --example nextjs

Go into the my-weather folder, and run npm i . Once it’s done, start the server with npm start .

You’ll see a page that displays information about a couple of SpaceX Dragon capsules (WunderGraph uses the SpaceX GraphQL API as an example). Ignore that, and go into the .wundergraph/wundergraph.config.ts file, remove the SpaceX API reference, and edit it to look like this, instead:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

I essentially removed any reference to the SpaceX API and added 2 new API integrations, the weather and the countries APIs, then added them to the apis dependency array.

This file contains the configuration for your WunderGraph BFF Server. Here, you’ll want to add all the APIs you want to compose together — and it doesn’t even have to be data dependencies like microservices, databases, or external APIs. WunderGraph also supports integrating auth via Clerk , Auth.js , or your own auth solution; and even S3-compatible storage for file uploads . All baked into the BFF layer.

Now, you can go to the .wundegraph/operations folder and create a file called CountryWeather.graphql where we’ll add a nice GraphQL operation that joins information from both APIs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

We’re essentially requesting the capital city of the country we give our operation, and then we’re asking for the weather within that city.

All you have to do now, is go to the index.tsx file, and use the useQuery hook to request the weather for your favorite country. In my case, I’m going to do:

1
2
3
4
5
6

And as a result, I’ll be getting this JSON:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

Both APIs were used, their responses collected and stitched together, but from the perspective of a frontend developer, they’d only have to call an endpoint on the BFF server — one that uses a persisted, hashed GraphQL query to get the data needed by the client’s UI/UX, serving the final response as JSON over RPC.

It’s like magic! (You can get the full source code for this project here if you want to review it in detail).

3. Watch out for the fan-out antipattern

With the BFF pattern, there is always a chance that you’ll end up with what is known as a “fan-out ” problem.

This is when your BFF is responsible for orchestrating API requests to multiple backend services, or third-party APIs — thus introducing multiple points of failure. If any of these called downstream services fail, experience issues, or are otherwise unavailable, it can cause cascading failures in the BFF, ultimately impacting the frontend user experience.

Potential solutions to these problems include:

  1. Circuit breaker pattern . With this pattern, you can handle faulty and failing backend services in a controlled manner. When a backend service experiences issues, the Circuit Breaker quickly prevents the BFF from sending additional requests, reducing the time spent waiting for unresponsive services. By isolating the failing backend service this way, the Circuit Breaker prevents cascading failures from affecting other parts of the BFF or frontend application. This ensures graceful degradation during backend unavailability.
  2. Caching. If a backend service is unavailable, the BFF can provide cached or default data instead of returning an error. Caching frequently accessed data can also help reduce dependencies on backend services and improve response times during periods of backend unavailability — especially when used in conjunction with a Circuit Breaker.
  3. Service monitoring. If you’re looking to understand how are your services being used (number of requests, most common requests, etc) then the BFF layer is a great place to implement logging, and monitoring the health of backend services and keep track of their availability and performance. The best part is that you can identify and resolve issues proactively with this insight.

4. Handle Errors Consistently on the BFF

In my previous example, any of those externals API could’ve failed. If they did, they’d all report errors in wildly different ways. You need to ensure your BFF knows how to handle these errors in a consistent, unified manner, and translate those errors back to the UI in meaningful ways.

In other words, take advantage of the BFF layer and use it as an error aggregator/translator. The client app can’t make sense of an HTTP 500 vs. an HTTP 200 that has an error in the JSON body — and it shouldn’t.

Instead, have the BFF layer standardize error handling. That includes the format of the response, but also its content (i.e. headers, error messages, etc). That way coding the client app to feedback errors to your users in meaningful ways becomes an easier task, since all your internal error states would now be canonical as far as the client is concerned.

5. Use a Node-based server so you can leverage TypeScript

A NodeJS-based server (Express.js, NestJS, or just WunderGraph’s BFF implementation — which uses a Node/Go server layer) for your BFF lets you use TypeScript on both the frontend and the backend. Embrace its benefits!

TypeScript is a superset of JavaScript that adds static typing, which adds a layer of safety — helping you catch errors early in development, and making your code more maintainable and easier to refactor. If you can use TypeScript to develop both the frontend and the “backend” (the BFF layer), you achieve language consistency throughout the application, making it easier for developers to switch between client and server code without context switching between languages.

As Sam Newman mentions in his blog post, ideally, the frontend team should have ownership of the BFF. Using a Node-based server and TypeScript for both frontend and BFF development, lets them have both in a monorepo, making development, iteration, maintenance, testing, and deployments easier.

In fact, TypeScript can also allow you to share certain code, types, and interfaces between the frontend and the “backend” (actually the BFF). For example, if you have validation logic that is shared between the client app and the BFF, TypeScript can ensure that these shared components are correctly used in both places.

Here is an article I wrote a while ago on how to do just that.

Conclusion

In the end, BFF is nothing fancy or strange, but rather, yet another layer of abstraction that seats between your UI and the core business logic.

By incorporating these five best practices, you’ll be well on your way to building scalable, maintainable, and performant BFFs, ensuring a great experience for both users and developers alike.

Have you ever used this pattern before? What about WunderGraph as a BFF framework? Share your thoughts in the comments, I’d love to know what you think about them