"API gateway", "reverse proxy", and "backend-for-frontend" are three terms that mean three different things in the literature and roughly the same thing in most production environments. The conflation is partly the fault of the vendors (who sell their products as all three) and partly the fault of teams who only need one and end up calling it whichever name sounds most architectural. The resulting design conversations are confusing because everyone is using the same words to mean different things.
I am writing this article because I keep seeing teams ship something they call an "API gateway" that is actually a reverse proxy with auth, or build a "BFF" that is actually a generic API gateway with a fancier name. Both can work; both can also lead to ownership confusion when the team that owns the gateway is not the team that needs to add a feature to it.
My stance: the three concepts are real and distinct, the boundaries matter for ownership and evolution, and most teams should pick one of the three deliberately rather than evolving into a hybrid by accident.
The three roles, briefly
Reverse proxy is the generic infrastructure piece: nginx, HAProxy, Envoy in pass-through mode, AWS ALB. It moves bytes from a client to one of several backends and back. It might do TLS termination, basic load balancing, request rewriting. It does not understand your API.
API gateway adds policy and per-route logic on top of the proxy: rate limiting, authentication, request validation, response transformation, API versioning, observability. It understands your API at the HTTP level. AWS API Gateway, Kong, Apigee, Tyk are commonly-marketed products in this space.
BFF (backend-for-frontend) is a separate service per client (web, iOS, Android), owned by the team that owns that client. It calls the underlying microservices and aggregates / reshapes responses for the specific client's needs. The web BFF might combine three backend calls into one response shaped for the desktop UI; the mobile BFF might omit fields the mobile client does not show, to save bandwidth.
These three are layered, not exclusive. A typical large architecture has a reverse proxy at the edge, an API gateway behind it, and a BFF per client behind that. A small architecture has one piece doing all three roles.
Reverse proxy: the most generic role
A reverse proxy's job is dumb forwarding. The configuration is a list of route patterns, each mapped to a backend pool. Incoming request matches the pattern, gets forwarded to a healthy backend in the pool, response is returned to the client.
That is a reverse proxy. It does TLS termination, path-based routing, and that is roughly it. nginx, HAProxy, Caddy, and Envoy can all do this. Most cloud load balancers (ALB, GCP LB, Azure App Gateway) can do it too with their basic configuration.
What a reverse proxy does well: it is cheap, fast, and well-understood. The configuration is declarative and the failure modes are familiar. For a system where the only requirement is "route requests to the right backend", a reverse proxy is sufficient.
What a reverse proxy does badly: anything that needs to understand the API. It cannot enforce that the request body matches a JSON schema, cannot rate-limit per API key, cannot transform a v1 response shape into a v2 shape. Some reverse proxies (Envoy especially) can be extended with WASM or Lua scripts to do these things, but at that point you are building an API gateway on top of a proxy.
API gateway: policy and contract
An API gateway is a reverse proxy with a richer policy layer. The features that earn the name:
The differentiator from a reverse proxy is that the gateway understands the API. "Reject requests where the JSON body fails schema validation" requires parsing the body, which a reverse proxy does not do. "Rate-limit per API key" requires extracting the key from a header and looking it up, which is more than path-based routing.
A specific gateway pattern worth highlighting: request authentication at the gateway, propagated as a verified claim to the backend. The gateway validates the JWT or session, extracts the user ID, and forwards the request to the backend with a header like X-User-Id: 42 (often signed with a shared secret to prevent forgery). Backend services trust that header and skip re-validating the token. This eliminates the "every service implements its own JWT validation" duplication and centralizes a hard-to-get-right piece of code in one place. The gateway is the only thing that needs to know about token signing keys, expiration logic, and revocation lists; backend services receive a clean "this is who is calling" assertion.
The gateway is owned by a platform or infrastructure team. Application teams expose APIs; the gateway team enforces the rules consistently across all of them. This is a strength (one place to add auth) and a coupling point (any new policy needs the gateway team's bandwidth).
API gateway products (AWS API Gateway, Kong, Apigee, Tyk, Zuul) all do roughly these things. The differences are in the management UI, the integration story (Kubernetes-native, AWS-native, etc.), and the extensibility model.
BFF: shaped per client
The BFF is owned by the client team, not the platform team. It is a service that exists specifically to serve one client (the web app, the mobile app, an embedded device) and shapes responses to that client's needs.
A canonical example: the home page on the web app shows the user's profile, recent orders, and personalized recommendations. Without a BFF, the web app makes three API calls (GET /users/me, GET /orders, GET /recommendations) and assembles the page in JavaScript. With a BFF, the web app makes one call (GET /web/home-page) which the BFF translates into the three backend calls, aggregates the responses, and returns one shape.
What the BFF lets you do:
- Reshape responses for the client. The mobile app does not need the user's full profile; it only needs name and avatar. The BFF returns just those fields, saving bandwidth.
- Aggregate calls. One round trip from the client instead of three. On a slow mobile network, this is a real performance win.
- Hide backend changes from the client. When the recommendations service moves from
/recommendationsto/recs/v2, the BFF absorbs the change; the web app keeps calling/web/home-page. - Implement client-specific logic. The web client wants the home page in HTML; the mobile client wants it as JSON. The BFF for each can return the appropriate shape.
The cost: a separate service per client. The web BFF, the mobile BFF, the partner-API BFF, all are different services with their own deploy pipelines. For a small team this is too much overhead. For a large product with multiple clients, it is the right separation of concerns.
A practical note: a BFF is often co-owned by the front-end team that builds the client. This is intentional. The BFF's API is the front-end team's API, shaped to their needs. If you put the BFF under the back-end platform team's ownership, the front-end team has to file tickets to get response shapes changed; that latency kills the BFF's value.
A trade-off that does not get enough attention: a BFF adds a hop. The web client calls the BFF; the BFF calls three backends; the BFF returns to the web client. That is at minimum three sequential network calls (parallel, in the BFF's case, but still bounded by the slowest backend). Without a BFF, the web client makes those three calls in parallel from the browser, with the same total latency. The BFF win is not always a latency win; it is a bandwidth and developer-experience win. The mobile case is different: cellular round trips are expensive, so collapsing three calls into one is a real latency savings. For desktop web on broadband, the BFF's latency benefit is small and the developer-experience benefit (one well-shaped endpoint per page) is the actual value. Scope the BFF accordingly; do not promise latency wins it cannot deliver.
How they actually compose
The full picture, for a large microservice architecture:
Each layer is owned by a different team and adds a specific kind of value. The reverse proxy is owned by ops; the API gateway by the platform team; the BFFs by the client teams; the services by domain teams.
For a smaller architecture:
One piece doing all three jobs. The ALB does TLS termination (reverse proxy role), basic auth (API gateway role), and the backend services either return the raw shape or each owns its own "shape for this client" logic (BFF role). This is fine for small teams. The composability problems only show up at scale.
When teams confuse the three, what goes wrong
Three failure modes I have seen:
- "Our API gateway has business logic in it." A team adds reshape-the-response logic to the gateway because it is "closer to the client". Now the gateway team owns business logic they did not write. The right answer was a BFF (owned by the client team) for that reshape.
- "Our BFF has policy logic in it." A BFF starts implementing rate limiting, auth checks, and request validation that should be in the gateway. Now those policies are duplicated across three BFFs (one per client) and inconsistent. The right answer was to put the policy in the gateway and let the BFFs trust the gateway's enforcement.
- "Our reverse proxy is doing API-shaped things." A team uses nginx with a hundred lines of Lua to do schema validation, JWT verification, and rate limiting. The Lua becomes unmaintainable. The right answer was to introduce an API gateway proper as a layer between the proxy and the services.
The principle: each layer should do its job and only its job. When a layer starts doing a layer above's job, the boundaries are wrong and the resulting code is harder to evolve.
What I would do at different scales
For a team of fewer than twenty engineers building a single client product: ALB or nginx as the reverse proxy, do auth and rate limiting in your application services or in middleware in the application framework. Skip the API gateway and BFF as separate components; the boilerplate of running them is more cost than benefit at this scale.
For a team of twenty to a hundred engineers with multiple services: introduce an API gateway as a real layer. Auth, rate limiting, schema validation in the gateway. Application services trust the gateway and focus on business logic. Skip BFFs for now; clients can call the gateway directly with the canonical API shape.
For a team of more than a hundred engineers with multiple distinct client surfaces: introduce BFFs per client, owned by the client team. The architecture now looks like the four-layer stack above. The complexity is justified by the scale.
Three names, three teams, three jobs
Naming matters. If you call something an API gateway, the team that owns it should be the platform team and the policies enforced there should be cross-cutting. If you call something a BFF, the team that owns it should be the client team and the logic there should be client-specific shaping. If you call something a reverse proxy, it should not have business logic in it. The reason the names matter is that they imply ownership, and ownership implies who can change the thing without coordination. A team that calls their service a "gateway" but treats it as a BFF will be surprised when the platform team rejects their feature request because the platform team is not on the hook for client-specific logic. Get the name right, get the ownership right, and the rest of the architecture follows.
