Backend for Frontend (BFF) Pattern for GraphQL APIs
Leveraging the BFF pattern was pretty much my motivation to start WunderGraph. I think it's a very powerful pattern to simplify web development, improve the user experiece of the end user, and keep the codebase maintainable.
Problem
When building a web application, you usually have a frontend and a backend. In fact, there's not just a single backend, but more often than not, you have multiple backends. For example, you might have a monolithic backend that manages most of your business logic. In addition, you might have a couple of microservices that provide additional functionality, such as a payment service or a notification service. It's quite likely that the marketing team wants to use a headless CMS so they can easily manage content. Next, we might want to delegate authentication & authorization to a dedicated service, so we might be using Keycloak, Auth0 or another service. Finally, we might want to use a couple of 3rd party APIs, such as Stripe, Twilio, or Sendgrid.
All in all, we end up with a lot of different backend services that we need to integrate into our frontend. We want to keep the frontend as simple as possible, and we want to do the integration in a way that is secure, maintainable and scalable.
We're experienced developers, so we are not interested in re-inventing the wheel. How can we solve this problem with as little effort as possible?
Solution
The BFF pattern is a great solution to this problem. The Backend for Frontend Pattern was first introduce by SoundCloud in 2011. The idea is to have a dedicated backend for each frontend.
We've taken this idea to the next level and abstracted away common use cases and patterns into a framework. This way, you can build on top of a strong foundation and don't have to worry about common pitfalls and edge cases. As we're building BFFs for Enterprises and Startups for a few years now, we've learned a lot about what works and what doesn't. We've build a dedicated website to document common BFF Patterns (opens in a new tab), so I'll only give you a quick overview here.
The most significant pattern that makes building BFFs so much easier and maintainable is the API Dependencies Pattern. We've adopted this pattern from how we handle code-dependencies and package management in general. Applied to APIs, it means that we can very easily manage the dependencies of our BFF.
Another important aspect of making BFFs maintainable is to build on top of open standards. We've chosen GraphQL as the "common denominator" for API dependency management. We support importing OpenAPI, SOAP. For file uplodas, you can use any S3 compatible storage provider, such as AWS S3, Minio, or DigitalOcean Spaces. For authentication, we support OpenID Connect.
Furthermore, we've made the decision that exposing a GraphQL API is not a good solution for most use cases. While still possible and generally not a bad idea, we've found that it's much more powerful to expose a JSON-RPC API. More on that in the examples section.
Another important decision we've made is how we handle configuration and extensibility of the BFF. We've looked at common patterns, like using JSON, YAML, or TOML files. We found that these solutions are not very developer friendly because the config files are not type-safe. Moreover, we wanted to use a wildely adopted language that every web developer is comfortable with. That's why we've decided to use TypeScript as our language of choice for configuration and to extend the BFF. We're now more than two years into this decision and we're very happy with it.
As we're quite experienced in the API Management space, we were also well aware of the limitations of existing API Gateway solutions. What we really didn't like about them is how little they cared about what happens with an API that's in front of the gateway. So instead of just focusing on the backend part, we've decided to offer a complete end-to-end solution that includes the frontend as well. This means that we're not just making it easy to build BFFs, but we're also making it super easy to integrate them into any frontend framework you might be using. We're doing so by generating a generic core client library in combination with framework specific bindings. This way, we can offer a great developer experience for any frontend framework.
Now that we understand how we think about BFFs on the conceptual level, let's give some examples of how an implementation could look like from a user's perspective.
Examples
First, we need to define our API dependencies
import {introspect,configureWunderGraphApplication} from '@wundergraph/sdk';
// introspect a REST API through an OpenAPI file
const jsp = introspect.openApiV2({
id: 'jsp',
apiNamespace: 'jsp',
source: {
kind: 'file',
filePath: '../json_placeholder.json',
},
});
// introspect a GraphQL API
const countries = introspect.graphql({
apiNamespace: 'countries',
url: 'https://countries.trevorblades.com/',
});
// introspect another GraphQL API
const weather = introspect.graphql({
id: 'weather',
apiNamespace: 'weather',
url: 'https://weather-api.wundergraph.com/',
});
// pass all introspected APIs to the BFF framework to generate the BFF
configureWunderGraphApplication({
apis: [jsp, countries, weather],
});
What's happening now behind the scenes is that we're applying the API Dependencies Pattern as well as the API Aggregation Pattern to combine all APIs into a unified API.
Next, we can define an Operation that we want to expose to the frontend. Doing so will leverage the Persisted Operations Pattern, one of the core pillars of WunderGraph.
# .wundergraph/operations/CountryWeather.graphql
query (
$continent: String!
# the @internal directive removes the $capital variable from the public API
# this means, the user can't set it manually
# this variable is our JOIN key
$capital: String! @internal
) {
countries_countries(filter: { continent: { eq: $continent } }) {
code
name
# using the @export directive, we can export the value of the field `capital` into the JOIN key ($capital)
capital @export(as: "capital")
# the _join field returns the type Query!
# it exists on every object type so you can everywhere in your Query documents
_join {
# once we're inside the _join field, we can use the $capital variable to join the weather API
weather_getCityByName(name: $capital) {
weather {
temperature {
max
}
summary {
title
description
}
}
}
}
}
}
Once this Operation is defined, we could call it e.g. using curl to get a list of all countries in Europe and their weather forecast.
http://localhost:9991/operations/CountryWeather?continent=Europe
But that's not all. We can now use the BFF derived Client Generation Pattern to generate a type-safe client library for our frontend. This client library will be generated based on the Operation we've defined above.
Using the generated client, we could do the following in our frontend, using React as an example.
const CountryWeather = () => {
const {data, loading, error} = useQuery({
operationName: 'CountryWeather',
input: {
continent: 'Europe',
},
});
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error.message}</div>;
}
return (
<div>
{data.countries_countries.map((country) => (
<div>
<h1>{country.name}</h1>
<h2>{country.capital}</h2>
<h3>{country._join.weather_getCityByName.weather.temperature.max}</h3>
<p>{country._join.weather_getCityByName.weather.summary.title}</p>
<p>{country._join.weather_getCityByName.weather.summary.description}</p>
</div>
))}
</div>
);
};
As you can see, we're able to build a BFF & client application with just a few lines of code. In similar vein, we could now add authentication, file uploads and more to our BFF. As this section is already quite long, we'll leave it at that for now.
I hope this section gave you a good overview of how a BFF framework could look like and what decisions we've made.