API Gateway and Backends for Frontends
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
Layering: Isolating Backend and Frontend establishes that Bookshop’s API sits behind a gateway. This page is about that gateway itself: why it exists, what it does, when to split it into several client-specific gateways instead of one, and which products or frameworks actually implement it.
Why clients should not call services directly
Once Bookshop is more than one deployable — Catalogue, Orders, Payments, Shipping, each its own service — a
naive client (bookshop-web, bookshop-android, or a partner integration) that calls those services directly runs
into three problems (Richardson ch. 8.1):
-
Many round-trips. Rendering a single order-confirmation screen might need data from Orders, Catalogue (book titles and covers), Shipping (delivery estimate), and Customers (loyalty status). A client making one HTTP call per service, over a mobile network, pays for every one of those round-trips in latency the user feels directly.
-
No encapsulation. Each service’s API becomes part of every client’s contract. Splitting Orders into two services later, or renaming an endpoint, now means coordinating a release across every client that called it — exactly the coupling independent deployability was supposed to remove.
-
Protocols unsuitable for external clients. Internal services may use gRPC, a binary protocol, or a messaging channel that makes perfect sense between services on the same network but that a browser or a mobile app cannot speak, or should not be trusted to reach directly.
The API gateway: a single entry point
The standard answer is an API gateway: a single entry point that every external request passes through before it reaches any service (Richardson ch. 8.2). A gateway typically does some combination of:
-
Request routing. Forward
/api/orders/**to Orders,/api/catalogue/**to Catalogue, and so on — one externally visible host name and path space in front of many internal deployables. -
API composition. Fan out a single incoming request to several services and combine their responses into one payload the client wants, so the client makes one call instead of three.
-
Edge functions. Centralize concerns that would otherwise be duplicated in every service: authentication and token validation, rate limiting, TLS termination, request logging and correlation-id injection, and often response caching.
-
Protocol translation. Accept HTTP/JSON (or GraphQL) from clients on the outside and translate to gRPC, binary messaging, or whatever protocol the internal services actually speak.
Benefits and drawbacks
Benefits: clients get encapsulation — they never see how many services exist behind the gateway, or how they are split — and fewer round-trips, since composition and caching happen at the edge instead of in the client.
Drawbacks: the gateway becomes one more highly-available component that every single request depends on, so it needs the same reliability engineering (redundancy, health checks, graceful degradation) as any other critical service; and because every team’s traffic flows through the same gateway configuration, it risks becoming a development bottleneck — a shared resource that every team needs to touch, and that nobody wants to be the one to break.
Backends for frontends
A single shared gateway assumes every client wants the same shape of response, which is rarely true for long: a browser SPA, a native mobile app, and a partner integration tend to want different payload sizes, different composition, and different release cadences. The backends-for-frontends (BFF) pattern splits the gateway into one gateway per client type, each one owned by the team that owns that client, rather than by a central platform team.
Netflix’s own experience with its API layer is the canonical story here: a single shared API gateway grew into a tangle of Groovy scripts trying to satisfy every device team’s slightly different needs from one shared codebase. Netflix’s fix was to give each client team its own NodeJS-based API module, deployed and owned independently, composing data with Falcor rather than routing every device through one monolithic gateway configuration. The lesson generalizes: once client teams are numerous enough, or different enough in what they need, a shared gateway becomes the same kind of bottleneck a shared monolith becomes for backend teams.
That said, one shared gateway is often enough — and simpler to run — when there are only one or two client types, when their response-shape needs are similar, or when the organization is small enough that a single platform team can serve every client without becoming a blocker. Splitting into BFFs is a response to a real symptom (a gateway team that cannot keep up, or clients fighting each other for backward-incompatible changes to the same endpoint), not a default to reach for on day one.
Implementation options
Off-the-shelf
Several products and platforms implement the gateway pattern out of the box, configured rather than coded. For the Azure-specific realization of this pattern, see Gateways and Load Balancing and API Management. For the AWS-specific realization, see Load Balancers and Gateways and API Gateway. For the Google Cloud-specific realization, see Load Balancing and Cloud Armor and API Gateway, Apigee and Cloud Endpoints.
-
AWS API Gateway — a managed gateway integrated with Lambda, API Gateway authorizers, and the rest of AWS.
-
Kong — an open-source gateway built on NGINX, with a plugin ecosystem for authentication, rate limiting, and transformations.
-
Traefik — a reverse proxy and gateway with native service-discovery integrations for Docker and Kubernetes.
-
Envoy and the Kubernetes Gateway API — the proxy that underlies most modern service meshes, and the Kubernetes-native API for configuring it declaratively.
-
NGINX — a general-purpose reverse proxy, widely used as a lightweight gateway in front of simpler deployments.
Building your own
When the routing and edge logic needs to be custom code rather than configuration, two framework-level options fit the JVM and .NET worlds Bookshop uses elsewhere in this section:
-
Spring Cloud Gateway — a Spring Boot application that routes, filters, and composes requests in Java, integrating with Spring Security and Spring Cloud service discovery.
-
YARP (Yet Another Reverse Proxy) — a .NET library for building a reverse proxy or gateway as an ASP.NET Core application, used elsewhere on this site as the .NET strangler-fig proxy of choice.
GraphQL as a gateway
A GraphQL layer can itself act as a gateway: a single schema composes fields from several underlying services, giving clients one query endpoint instead of one REST call per service. Whether that is the right fit for Bookshop depends on the same trade-offs When to Use GraphQL or REST covers; when several services each own part of the schema, GraphQL Federation is the composition mechanism.
The gateway during a migration
While Bookshop is being extracted from a monolith into services, the gateway is not just a runtime concern — it becomes the strangler’s routing seam: the single place that decides, request by request, whether a given path is served by the newly extracted service or still falls through to the monolith. Every migration path in this section relies on that seam; Migration Path: From a Legacy Stack to a Modern One covers routing by path, header, or user cohort in depth.
Figures
One shared gateway serves every client from a single configuration; the BFF variant gives each client type its own gateway, owned by that client’s own team, calling only the services that client actually needs.
A Spring Cloud Gateway route configuration reflecting Bookshop’s extraction in progress — Orders already split
into bookshop-orders, everything else still served by the bookshop monolith:
spring:
cloud:
gateway:
routes:
- id: bookshop-orders
uri: lb://bookshop-orders
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=0
- name: CircuitBreaker
args:
name: bookshopOrdersCircuitBreaker
fallbackUri: forward:/fallback/orders
- id: bookshop-monolith
uri: lb://bookshop
predicates:
- Path=/**
filters:
- StripPrefix=0
/api/orders/** resolves to the extracted bookshop-orders service; every other path — Catalogue, Payments,
Shipping, and anything not yet extracted — still falls through to the bookshop monolith behind the same route
table. As each capability is extracted, only this configuration changes; clients never see the difference. The
CircuitBreaker filter is explained in Gateway & OpenFeign.
Rate limiting at the gateway, per client and shared by every gateway replica, is covered in
Bucket4j: Spring Cloud Gateway.