GraphQL vs REST in a BFF Layer: Which Should You Use and When?
The Backend-for-Frontend pattern solves the mismatch between what upstream services expose and what clients actually need. The BFF sits between your clients and your services, composing, shaping, and optimising data for each channel. But once you have decided to build a BFF, a second decision immediately follows: should the API surface you expose to clients be REST or GraphQL?
This is not a trivial choice. The decision affects how your clients fetch data, how your BFF caches responses, how you version and evolve the API, how much tooling you need to maintain, and how quickly your frontend teams can ship features independently. The wrong choice for your context does not break things immediately — it compounds, slowly making every subsequent feature harder to build and every performance optimisation harder to implement.
This post works through the tradeoffs with concrete e-commerce examples, because e-commerce is where both patterns are genuinely used in production and where the differences become clearest. By the end you will have a framework for making the decision in your own context — not a blanket recommendation, because the right answer genuinely depends on factors specific to your team and product.
What the BFF Layer Is Actually Doing
Before comparing the two API styles, it is worth being precise about what a BFF layer does in an e-commerce system. A product page request needs data from a product catalogue service, an inventory system, a pricing engine, a recommendation service, and a review aggregator. These are five separate upstream services, each with its own schema, authentication, and response shape.
The BFF receives the client request, fans out to all five services in parallel, merges the results, filters to the fields the client actually needs for this channel, and returns a single response. The client makes one call; the BFF makes five. This is the composition and shaping work that the BFF performs regardless of whether the surface it exposes is REST or GraphQL.
The REST vs GraphQL question is therefore specifically about the contract between the BFF and its clients — not about what the BFF does internally. Your BFF's upstream calls to the catalogue service, inventory API, and pricing engine will almost certainly be REST calls regardless of which style you choose for the client-facing surface. The decision is purely about the outward-facing API.
The Case for REST in Your BFF
REST remains the right choice for most BFF layers, and the reasons are more practical than theoretical. REST endpoints map naturally to the bounded contexts your BFF serves: a product page endpoint, a cart endpoint, a checkout endpoint, an order history endpoint. Each endpoint is purpose-built for its use case, returns exactly the fields that use case needs, and is independently cacheable at the HTTP layer.
HTTP caching is REST's biggest advantage at the BFF layer. A GET request to /bff/product-page/p-421 can be cached at the CDN, at a reverse proxy, or at the BFF's own response cache with a standard TTL. When 10,000 users load the same product page in the same minute, the BFF upstream calls happen once (or a handful of times at cache boundaries), not 10,000 times. GraphQL queries sent as POST requests — the dominant pattern — cannot be cached this way. Every GraphQL mutation and most queries bypass HTTP caching infrastructure entirely, pushing the caching problem into the application layer where it is more complex to implement and easier to get wrong.
REST BFF endpoints are also simpler to reason about for on-call engineers. When a product page is slow, you check the /bff/product-page endpoint in your monitoring dashboard, see which upstream calls are slow, and diagnose the problem. There is no query introspection needed, no variable inspection, no schema resolver tracing. The execution model is transparent: this endpoint makes these calls, in this order, with these timeout policies.
For mobile clients in particular, REST BFF endpoints offer a significant advantage: you control the payload size. A mobile BFF endpoint returns only the fields a mobile screen can display. An image field returns a small image URL; the web BFF for the same product returns a high-resolution URL. This is not something a client-defined GraphQL query can achieve as elegantly — the client specifying image { url } still gets the full-resolution URL unless the BFF resolver applies per-client logic, which reintroduces server-side channel awareness in a less explicit form.
REST BFF: Where It Gets Painful
REST BFFs have a well-known failure mode: endpoint proliferation driven by UI requirements. A product page has a REST endpoint. Then the product page adds a new section that needs three extra fields. The frontend team adds those fields to the endpoint. Then a feature is removed from one platform but not others, and the fields stay in the response forever because removing them would be a breaking change. Six months later the BFF has 40 endpoints, half of which return fields nobody uses, and every new frontend feature requires a backend change to add or modify a field.
This is the over-fetching and under-fetching problem that GraphQL was designed to solve. In REST BFFs it manifests as:
- Over-fetching: The product list endpoint returns 40 fields because the product detail page needs 40 fields, but the product list UI only displays 8. Every product list load transfers 5× more data than the client uses.
- Under-fetching: The checkout summary endpoint does not return the customer's loyalty tier, so the checkout UI makes a separate call to the account endpoint. Now you have two BFF calls for one UI view — the exact coordination problem the BFF was supposed to solve.
- Frontend-backend coupling: Every new UI feature that needs different data requires a backend deployment to add or adjust a BFF endpoint. Frontend and backend teams cannot move independently.
The honest response to these problems in a REST BFF is disciplined endpoint design: create separate endpoints for each distinct UI view (product card, product detail, product comparison), resist the temptation to reuse endpoints across contexts just because they return overlapping data, and treat the BFF endpoints as a UI-specific schema, not a general-purpose API. This is achievable but requires ongoing discipline that not all teams maintain.
The Case for GraphQL in Your BFF
GraphQL at the BFF layer solves the frontend-backend coupling problem directly. When the frontend team needs a new field, they add it to the query — no backend deployment required, no coordination with the BFF team, no waiting for a sprint cycle. The BFF exposes a schema; the client declares what it needs; the BFF resolves it. This is the fundamental value proposition.
In practice this advantage is most pronounced when you have a single large engineering organisation where frontend teams are moving faster than backend teams, or where multiple frontend teams (web, mobile, partner portal) are actively developing simultaneously and cannot wait on each other's BFF endpoint requests. A GraphQL BFF allows each team to iterate on their queries independently, without introducing deployment dependencies on the BFF layer.
GraphQL also handles the product comparison use case elegantly. Comparing three products means fetching a consistent set of fields for all three. A REST endpoint optimised for a single product page returns fields in a shape that does not compose naturally for comparison. A GraphQL query fetches exactly the fields needed for comparison across all three products in a single request, and the query is self-documenting about its intent.
For a composable commerce platform with a deep product catalogue — products with hundreds of configurable attributes, variant combinations, and context-specific pricing — GraphQL's schema-driven approach handles the heterogeneity better than a fixed REST response shape. A product with 20 configurable attributes and a product with 3 can share the same GraphQL type without either over-fetching or requiring separate endpoints.
GraphQL BFF: Where It Gets Painful
The practical difficulties of GraphQL at the BFF layer are less discussed than its theoretical advantages. The first and most significant is the N+1 problem. When a client queries a product list and includes a field that triggers a per-product resolver — inventory status, for example — the naive GraphQL resolver makes one inventory API call per product. A list of 20 products generates 20 inventory calls rather than one batched call. DataLoader patterns and resolver-level caching solve this, but they add complexity to the BFF implementation that a REST BFF with explicit parallel batching avoids entirely.
The second difficulty is caching. As noted above, GraphQL queries sent as POST requests do not benefit from HTTP caching. Application-level caching can compensate — caching resolver results by field and argument — but this is significantly more complex to implement correctly than HTTP response caching. It also creates subtle correctness bugs: a cached inventory resolver result for product A might serve stale data to a query that did not expect caching at the resolver level. Persisted queries (converting frequently-used queries to GET requests with a query ID) partially address this but add a separate client-server coordination mechanism.
Third, arbitrary client queries create unpredictable server load. A client can construct a query that joins deeply nested types, resolves many relationships, and triggers dozens of upstream service calls. In a REST BFF, every endpoint has a known, bounded set of upstream calls. In a GraphQL BFF, a malicious or poorly-written client query can trigger an arbitrary fan-out. Query depth limiting, complexity analysis, and field-level rate limiting are necessary production controls, but they add operational overhead that a REST BFF does not require.
Fourth, tooling and observability are harder. In a REST BFF, you monitor /bff/product-page and/bff/checkout as distinct operations in your APM. In a GraphQL BFF, all traffic hits a single /graphql endpoint. Distinguishing slow product page queries from slow checkout queries requires GraphQL-aware tracing at the operation name level — which requires every client query to have a named operation. This is achievable but requires discipline from every team writing queries.
A Concrete E-Commerce Comparison
Consider a product listing page in a headless Shopify or Elastic Path implementation. The page displays a grid of product cards, each showing: name, primary image, price (customer-specific), stock status, and a promotional badge if applicable.
With a REST BFF: The frontend calls/bff/product-list?category=shoes&page=1&customerId=c-99. The BFF fans out in parallel to the catalogue service (product names and images), the pricing service (customer-specific prices for these 20 products), and the promotion service (applicable badges). It merges the results and returns exactly the 5 fields per product the card UI needs. The response is cached at the CDN by category + page, with the customer-specific price computed server-side and returned personalised. Response time: ~90ms.
With a GraphQL BFF: The frontend sends a query to /graphql specifyingproducts(category: "shoes", page: 1) { name image { url } price(customerId: "c-99") stockStatus promotion { badge } }. The BFF resolves each field. price andstockStatus are per-product resolvers — without DataLoader batching, this generates 20 pricing calls and 20 inventory calls. With DataLoader, it batches to 1 each. The response is not cached at the CDN (POST request). Application-level caching on the stock resolver might serve stale data. Response time: ~110ms with DataLoader, ~400ms without, and no CDN cache hit benefit.
The REST BFF wins on this use case. Now consider the product detail page, which includes a configurable product with 30 variant attributes, and the frontend team wants to add the sustainability score field — a new field from a new upstream service — without waiting for a BFF deployment.
With REST: Adding the sustainability score requires a BFF code change, a PR, a review, and a deployment. The frontend team waits 1–3 days. Meanwhile they build the UI component against a mock.
With GraphQL: The schema already exposes a sustainabilityScore field on the product type (assuming the BFF schema was designed with sufficient coverage). The frontend team adds it to their query and ships. No BFF deployment needed. This is where GraphQL's value is most tangible.
Versioning: REST vs GraphQL at the BFF Boundary
API versioning is where many BFF teams underestimate the difference between the two approaches. REST API versioning has a well-understood pattern: route-based versioning (/v1/bff/product-page,/v2/bff/product-page) with explicit deprecation timelines. Both versions run simultaneously, old clients continue working, and the migration is deliberate and tracked.
GraphQL handles versioning differently — the schema is designed to evolve without breaking changes by adding fields rather than modifying existing ones, and by deprecating fields with @deprecated directives. This works well when the schema is designed carefully from the start. It works poorly when early schema decisions were wrong — a field that returns the wrong type, a type whose name conflates two different concepts, a mutation that has the wrong argument structure. Fixing these requires either a breaking change (which GraphQL's approach explicitly avoids) or maintaining an awkward schema forever.
For a BFF layer specifically, the versioning question is whether your clients are in an environment where you can coordinate upgrades. A web frontend you deploy continuously can adopt a new BFF query shape immediately. A native mobile app with users still running a version from 18 months ago cannot. If your BFF serves mobile clients you do not control, REST versioning's explicit/v1 and/v2 paths are operationally simpler than managing a GraphQL schema that must be backward-compatible indefinitely.
The Hybrid Approach: REST Orchestration, GraphQL Surface
The most pragmatic architecture for large commerce platforms is a hybrid: the BFF's internal orchestration is REST-based (calling upstream services via REST, using parallel execution and caching), while the surface exposed to web clients is GraphQL and the surface exposed to mobile clients is purpose-built REST endpoints.
This hybrid is not a compromise — it is the right tool for each job. The orchestration layer benefits from REST's simplicity, explicit batching, and HTTP caching for upstream calls. The web frontend benefits from GraphQL's flexibility to iterate on queries without BFF deployments. The mobile app benefits from purpose-built REST endpoints with predictable payloads and CDN cacheability.
The orchestration layer — the component that fans out to upstream services, manages connection pools, applies caching policies, and handles partial failures — is the same regardless of the surface protocol. This is where an API orchestration platform adds value: it handles the upstream coordination regardless of whether the BFF surface is REST or GraphQL, so the choice of surface protocol becomes a pure API design decision rather than an infrastructure decision.
The Decision Framework
Use this framework to make the choice for your specific context. The honest answer for most teams is REST, with a narrow set of circumstances where GraphQL genuinely pays off.
Choose REST for your BFF surface when:
- — Your primary clients are mobile apps with version lag
- — CDN caching is important (high traffic, read-heavy product pages)
- — Your team is small and the BFF-frontend coordination overhead is low
- — You have a focused set of well-defined UI views that change infrequently
- — Observability and on-call simplicity are priorities
- — You are building a partner-facing BFF with a stable, published contract
Choose GraphQL for your BFF surface when:
- — Multiple frontend teams are iterating rapidly and independently
- — Your product data is highly heterogeneous (complex configurable products)
- — Frontend deployment velocity is blocked by BFF deployment cycles
- — Your clients are web frontends you deploy continuously (not mobile with version lag)
- — You have engineers with GraphQL production experience and can implement DataLoader correctly
- — You are willing to invest in GraphQL-aware monitoring and query complexity controls
Choose hybrid (REST orchestration + GraphQL surface for web, REST for mobile) when:
- — You have both web and mobile clients with different iteration speeds
- — The orchestration layer's performance and caching requirements are non-negotiable
- — You want frontend flexibility for web without sacrificing mobile performance
What Does Not Change Regardless of Your Choice
The underlying orchestration work is identical whether you expose REST or GraphQL. Fanning out to multiple upstream services in parallel, handling partial failures, applying caching policies per upstream call, managing credentials and connection pools, and producing execution traces for observability — none of this changes based on the client-facing protocol.
The most common mistake teams make when choosing GraphQL for their BFF is conflating the surface protocol with the orchestration layer. GraphQL does not automatically make your BFF faster, more reliable, or better observable. It changes the contract with your clients. A GraphQL BFF still needs all the same orchestration infrastructure: parallel upstream calls, retry policies, caching, connection pooling, and execution tracing. A GraphQL resolver that calls an upstream service sequentially and does not cache repeated calls will be slower and less reliable than a REST BFF endpoint that uses proper parallel execution and response caching.
The protocol choice is the last decision in BFF design, not the first. Get the orchestration architecture right first — parallel execution, caching strategies, failure policies, observability. Then choose REST or GraphQL as the surface based on your specific client requirements and team dynamics.
Related reading
- Building Modern BFF Layers: Backend-for-Frontend Architecture — the architectural foundation before you choose REST or GraphQL
- BFF Characteristics: Parallel Execution, Caching, Observability & Alerts — what the orchestration layer needs regardless of surface protocol
- Parallel API Calls: How to Cut Response Times by 60% — the upstream fan-out technique that applies to both REST and GraphQL BFFs
- BFF Use Cases in E-Commerce: Tax Calculation, Pricing, Inventory & More — the e-commerce workflows where BFF design decisions have the most impact
REST: CDN-Cacheable by Default
REST BFF endpoints respond to GET requests that CDNs and reverse proxies cache automatically. At high traffic, a single BFF call populates the cache for thousands of concurrent users — no application-layer caching infrastructure required.
GraphQL: Frontend Independence
GraphQL BFF surfaces let frontend teams add fields to their queries without BFF deployments. When multiple teams are iterating rapidly and cannot wait on backend sprint cycles, this independence directly translates to shipping velocity.
Orchestration Stays the Same
Parallel upstream execution, connection pooling, retry policies, and caching per upstream call are the same whether your BFF surface is REST or GraphQL. The protocol choice is a client contract decision, not an orchestration decision.
GraphQL Needs Query Controls
Production GraphQL BFFs require depth limiting, query complexity analysis, operation naming for observability, DataLoader for batching, and field-level rate limiting. These are solved problems but add operational overhead that a REST BFF avoids.
Build your BFF layer — REST or GraphQL
Apitide's orchestration engine handles the upstream fan-out, parallel execution, caching, and observability regardless of whether your BFF surface is REST or GraphQL — so you can make the protocol choice based on your clients, not your infrastructure.