Fixing the Backends-for-frontends pattern with Build-time GraphQL
The BFF pattern optimizes backends for specific client interfaces. Using GraphQL for it fixes some of its drawbacks, but can we do better? Letâs find out with WunderGraph.
The Backends-for-frontends (BFF) pattern, first described by Sam Newman, and first used at SoundCloud, is an effective solution when you have multiple client platformsâ -- web browsers, mobile apps, gaming consoles, IoT devicesâ -- âeach with unique requirements for data, caching, auth, and more.
The BFF is a custom interface (maintained by the frontend team) that is purpose built for the needs of each client platform, serving as the only âbackendâ as far as the client is concerned. It sits between the client and underlying data sources/microservices, keeping them decoupled, and giving each client team full control over how they consume, consolidate, and massage data from downstream API dependenciesâ -- and handle their errors.
But the BFF pattern, like anything else in engineering, has its tradeoffs and drawbacks. As it turns out, though, GraphQL is great at shoring up these flaws, and a GraphQL driven BFF layer works well. However, GraphQL brings with it its own set of problems!
Starting to sound like a broken record, here. How do we break the cycle? Letâs talk about it, with a look at a free and open-source technologyâ -- âWunderGraphâ -- that can help us.
Backends-for-Frontends ⤠GraphQL
BFFs afford you the luxury of maintaining and securing only a single entry point like you do in simple monoliths, while still working with microservices internally. This gives the best of both worlds, but that kind of agility comes at a price. Letâs take a look at some of these issues, and how GraphQL nullifies them:
1. BFFs work great as an aggregation layer, but you still need multiple requests to get the data you want, and this scales linearly with the number of Microservices/API/Data dependencies you have. Multiple network hops add latency.
GraphQL works great in minimizing multiple round trips because it natively enables clients to request multiple resources and relationships in a single query.
2. Overfetching and underfetching might still be problems because of a combination of how RESTful architectures inherently operate, and the fact that you may be relying on legacy or third-party APIs whose architectures you do not own.
Image Source: https://www.howtographql.com/basics/1-graphql-is-the-better-rest/
In RESTful APIs, itâs the server that determines the structure and granularity of the data returned, and this can lead to more data than the client actually needs.
GraphQL shifts that responsibility to the client, and they can specify exactly the data they need in a single query.
Image Source: https://www.howtographql.com/basics/1-graphql-is-the-better-rest/
Whether or not your downstream microservices/APIs are using GraphQL themselves doesnât matter for the client/BFF interaction. You wonât need to rewrite anything to reap the benefits.
3. Documentation is going to be an ongoing chore. A BFF is purpose-built for a specific client, and so each BFF will need detailed accompanying API documentation (with payload/response specifications) to be created and kept up-to-date. If using REST, the endpoint names and HTTP verbs will only grow with the app and its complexity, making them difficult to document, and thus difficult to onboard new hires for.
GraphQL is declarative and self-documenting by nature. Thereâs a single endpoint, and all available data, relationships, and APIs can be explored and consumed by client teams (via the GraphiQL interface or just Introspection) without constantly going back and forth with backend teams.
âŚbut GraphQL isnât a silver bullet.
Adopting GraphQL shores up some BFF issues, no doubt, but GraphQL adoption isnât something to be taken lightly. Itâs not just another UI library to add to your tech stack, and much like anything in engineering, it has growing pains, trade-offs, and complexities associated with it.
1. GraphQL has a steep learning curve. Understanding its conceptsâ -- schemas, types, queries, mutations, and subscriptionsâ -- ârequires substantial time investment, and writing good resolver functions requires a good understanding of the underlying data models and how to retrieve and manipulate data from different sources (databases, APIs, or services). Server-side data coalescing is a neat idea, but this makes your GraphQL implementation only as good as your resolvers, and involves a ton of boilerplate that can make the implementation more verbose and error prone.
2. GraphQL has no built-in caching. The simplicity that GraphQLâs single endpoint format brings to the table can also become a bottleneck in certain circumstancesâ -- âREST APIsâ multiple endpoints allow them to use HTTP caching to avoid reloading resources, but with GraphQL, youâre only using one universal endpoint for everything, and will have to rely on external libraries (like Apollo) for caching.
3. GraphQL clients can be pretty heavy in terms of bundle size that you have to ship. This might not matter in some use-cases (if youâre only ever building an internal app, for example) but it can and does impact the load time and performance of your application, especially in scenarios where low bandwidth or slow network connections are a concern.
4. Performance can be a concern, if youâre not careful. The nature of GraphQL makes it non-trivial to harden your system against drive-by downloads of your entire database. Itâs far more difficult to enforce rate limits in GraphQL than it is in REST, and a single query that is too complex, too nested, fetching too much data, could bring your entire backend to a crawl. Even accidentally! To mitigate this, youâll need an algorithm to calculate a queryâs cost before fetching any data at allâ -- and thatâs additional dev work. (For reference, this is how GitHubâs GraphQL API does it)
5. GraphQL doesnât have a built-in way to ensure security. RESTâs vast popularity and authentication methods make it a better option for security reasons than GraphQL. While REST has built-in HTTP authentication methods, with GraphQL the user must figure out their own security methods in the business layer, whether authentication or authorization. Again, libraries can help, but thatâs just adding more complexity to your builds.
Introducing WunderGraph.
WunderGraph is a free and open-source (Apache 2.0 license) framework for building Backends-for-Frontends.
GitHub - wundergraph/wundergraph: WunderGraph is a Backend for Frontend Framework to optimizeâŚ
_WunderGraph is a Backend for Frontend Framework to optimize frontend, fullstack and backend developer workflows throughâŚ_github.com
Using WunderGraph, you define a number of heterogeneous data dependenciesâ -- internal microservices, databases, as well as third-party APIsâ -- âas config-as-code, and it will introspect each, aggregating and abstracting them into a namespaced virtual graph.
Hereâs what a system you might build with WunderGraph would look like.
Letâs dive right in. First of all, youâll have to name your APIs and services as data dependencies, much like you would add project dependencies to a package.json
file.
./.wundergraph/wundergraph.config.ts
// Data dependency #1 - Internal database
const db = introspect.postgresql({
apiNamespace: "db",
databaseURL: "postgresql://myusername:mypassword@localhost:5432/postgres",
});
// Data dependency #2 - Third party API
const countries = introspect.graphql({
apiNamespace: "countries",
url: "https://countries.trevorblades.com/",
});
// Add both to dependency array
configureWunderGraphApplication({
apis: [db, countries],
});
You can then write GraphQL operations (queries, mutations, subscriptions) for getting the data you want from that consolidated virtual graph.
./.wundergraph/operations/CountryByID.graphql
query CountryByID($ID: ID!){
country: countries_country(code: $ID) {
name
capital
}
}
./.wundergraph/operations/Users.graphql
query Users($limit: Int!){
users: db_findManyusers(take: $limit) {
username
email
}
}
âŚand WunderGraph will automatically generate endpoints that you can cURL to execute these queries and return the data you want, over JSON-RPC.
> $ curl http://localhost:9991/operations/CountryByID?ID=NZ
> {"data":{"country":{"name":"New Zealand","capital":"Wellington"}}}
> $ curl http://localhost:9991/operations/Users?limit=5
>{"data":{"users":[{"username":"user1","email":"user1@example.com"},{"username":"user2","email":"user2@example.com"},{"username":"user3","email":"user3@example.com"},{"username":"user4","email":"user4@example.com"},{"username":"user5","email":"user5@example.com"}]}}
If youâre using data fetching libraries like SWR or Tanstack Query, or a React-based framework (NextJS, Remix, etc.) you get even more out of WunderGraph, as it auto generates a lean, performant, custom TypeScript client for you every time you define an operation, complete with fully type-safe hooks for querying and mutating data that you can use on the frontend, wherever you need it.
./pages/index.tsx
// Import data fetching hook from WunderGraph-generated client
import { useQuery } from "../components/generated/nextjs";
const Home: NextPage = () => {
// Data dependency #1 - Internal Database
const { data: usersData } = useQuery({
operationName: "Users",
input: {
limit: 5,
},
});
// Data dependency #2 - Third-party API
const { data: countryData } = useQuery({
operationName: "CountryByID",
input: {
ID: "NZ",
},
});
//...
}
Now, you can use usersData
and countryData
however you want. Render it out into cards, lists, pass it on to props, go wild.
But how does WunderGraph address GraphQLâs issues?
Easilyâ -- âWunderGraph takes GraphQL completely out of the runtime.
Yes, you do write GraphQL operations to collect and aggregate the data you want from multiple data sources⌠but thatâs it. You wonât have to write resolvers, or worry about making them efficient at data fetching. Your involvement with GraphQL goes only as far as writing queries, and only during development. No public GraphQL endpoint is ever exposed, nor is any heavy GraphQL client shipped on the client.
Donât worry if this doesnât click for you immediately. I promise itâll make sense soon. Letâs look at this process in detail.
GraphQL only during local development?
Whenever you write a GraphQL query, mutation, or subscription and hit save, the Query is NOT saved as is. Instead, it is hashed and then saved on the server, only able to be accessed by the client by calling for the hashed name (together with input variables, if needed, of course).
This is what is called a Persisted Query. Your WunderGraph BFF server has a list of known hashesâ -- âas many Persisted Queries as you defineâ -- and will only respond to client requests for valid hashes, serving them as JSON over RPC, with a unique endpoint for each and every operation. (And WunderGraph generates typesafe hooks for React frameworks and data fetching libraries that do the same thing)
What does this mean? Two things.
- You address the security concerns associated with having your GraphQL API reverse engineered from browser network requestsâ -- âbecause there is no GraphQL API.
- If each operation has a unique endpoint, you can finally implement traditional REST-style caching! But thatâs not all. The WunderGraph BFF server generates an Entity Tag (ETag) for each responseâ -- âthis is an unique identifier for the content of said responseâ -- and if the client makes a subsequent request to the same endpoint, and its ETag matches the current ETag on the server, it means the content hasnât changed. Then the server can just respond with a HTTP 304 âNot Modifiedâ, meaning the clientâs cached version is still valid. This makes clientside stale-while-revalidate strategies blazingly fast.
You can write all the arbitrary queries you want using GraphQL during development time, but theyâre hashed and persisted at compile time, and only show up at runtime as JSON RPC. In one fell swoop, you have solved the most common issues with GraphQL, creating a best-of-both-worlds scenario. All the wins of GraphQL, with none of the drawbacks.
But what about the performance concerns of too-complex queries?
WunderGraph uses the Prisma ORM internally, crafting optimized queries under the hood for each Operation you write, no matter your data source. You wonât have to worry about hand-rolling your own SQL or GraphQL queries and sanitizing them to make sure a single naĂŻve query doesnât kill your downstream services, or lead to DOS attacks.
Where To Go From Here?
Youâve seen how WunderGraph makes building BFFs so much easier, with fantastic devex to boot. Using Prisma under the hood, it creates persisted queries and serves them over JSON-RPC, reining GraphQL back to only being used in local development.
But you can do far, far more than that with WunderGraph -- âitâs a true BFF framework:
- It supports fully typesafe development from front to back. Every Operation you write will generate a typesafe client/hook to access it from the frontendâŚand you can even generate typesafe Mocks this way! With WunderGraph, you have shared types between the backend and the frontendâ -- âgiving you full IDE autocomplete and inference both while writing GraphQL queries, and when developing the client.
- It can turn any query you write into a Live Query without even needing WebSocketsâ -- âmaking WunderGraph ideal for building BFFs which get deployed together with the frontend to serverless platforms, which canât use WebSockets.
- It can also integrate S3-compatible storage for file uploads, OIDC/non-OIDC compatible auth providers, and more.
- If you donât want to write GraphQL at all, you can write custom TypeScript functionsâ -- âasync resolvers, essentiallyâ -- to define Operations during development. You can perform JSON Schema validation within them using Zod, and aggregate, process, and massage the data any way you want, and then return it to the client. TypeScript Operations are are accessed exactly like GraphQL Operations are (JSON RPC endpoints, or clientside hooks), and run entirely on the server and are never exposed clientside. Plus, you can even import and use existing GraphQL operations within them.
- You can bake cross-cutting concerns that are ideally present on the BFF layerâ -- most importantly, authâ -- âinto WunderGraphâs BFF layer via Clerk/Auth.js/your own auth solution, and every Operation can be authentication aware.
With powerful introspection that turns API, database, and microservices into dependencies, much like a package manager like NPM would, WunderGraph makes BFF development accessible and joyful, without ever compromising on end-to-end typesafety. To know more, check out their docs here, and their Discord community here.
Top comments (0)