Understanding Backend for Frontend
Let's begin with a quick overview of API Gateway. This is a service that acts as an intermediary between clients and backend services, exposing APIs to the outside world.

Its core responsibilities include:
- acting as the single entry point between a backend split into numerous services and external consumers,
- concealing the internal service decomposition (from the client's viewpoint, the backend hidden behind the gateway appears as a monolithic system),
- functioning as a reverse proxy for backend services,
- providing firewall capabilities,
- enabling comprehensive network traffic monitoring,
- supporting caching, compression, load balancing, and A/B testing,
- handling authentication,
- performing action aggregation (combining multiple requests to various microservices and merging their responses within the context of a single client request).
Given the last two points, it becomes clear that this is more than a mere proxy. The API gateway's role extends beyond that of a simple nginx configuration — it is a fully-fledged backend service that incorporates application logic.
Backend for Frontend is a specialized adaptation of the API gateway pattern. Its defining characteristic is that a dedicated gateway (bff) is established for each client — where "client" refers to a distinct application, such as a web app, a mobile app, or two web applications with differing functionalities.

How does this differ from the classic gateway approach:
- each client-bff pair can establish a unique, customized API contract between them,
- the logic that was previously centralized in a single gateway gets distributed across multiple services based on their purpose,
- the failure of one bff does not disconnect other clients from the system.
Backend for Frontend, but Built by Frontend
Now, let's push this concept further and shift the responsibility for creating and maintaining a dedicated bff to the frontend team (the client-bff pairing is tightly coupled, and when scaling to multiple clients, the division of responsibilities follows the pattern shown below).

This shift brings certain implications. Most notably, the arena of contract negotiation between frontend and backend moves to the contracts established between bffs and microservices. The contracts and all communication between the client and the bff become an internal concern for the FE team. Moreover, this added responsibility requires at least a fundamental understanding of developing and operating server-side applications. The FE team, at the cost of its own resources, absorbs work that would otherwise fall on the backend team — which matters when backend personnel are a bottleneck in the project.
Maximizing the client and bff pairing
Our established and proven technology stack can be leveraged here:



When dealing with multiple web clients within a single system (for instance, the common case of a client-facing web application paired with an admin panel for management), adopting a monorepo approach proves highly effective.

Housing a web application alongside its bff within one workspace offers advantages such as complete typescript typing support (which helps maintain contract consistency between them). And when multiple client-bff pairs share the same workspace, sharing reusable modules becomes straightforward.
Building the BFF
NestJS, similar to Angular, relies on decomposing the application into modules. In our setup, we suggest structuring modules to mirror how the backend is split into microservices (one Nest module per microservice). Beyond that, the bff will include additional modules, such as:
- a module for handling (pre-)authentication,
- a proxy module (to simply forward all requests that don't require any aggregation on the bff side).

Inside each module, the application can be broken down into use-cases, each made up of a controller-service pair. A single use-case takes care of one request arriving from the client. For further aggregation, each use-case can expose its functionality to other use-cases (either by directly injecting the service or by introducing an additional facade layer).

The aggregation approach works as follows: the use-case service not only executes the call to the backend microservice but also leverages functionality provided by other use-cases, fetches supplementary resources, and combines them to craft the response to the client's request (an example of aggregating resources A and B into a single AB response is illustrated below).

The implementation of each use-case service follows a consistent pattern, comprising:
- importing the API contract,
- injecting facades (or other services directly),
- retrieving the primary resource,
- extracting identifiers for additional resources,
- fetching those additional resources (concurrently whenever feasible),
- combining the resources into the client-facing response (aligned with the contract).
...
// import bff Response type from client-bff API contract
import { GetBookDetailsResponse } from '@project-name/shared/contracts';
@Injectable()
export class GetBookDetailsQuery {
constructor(
private readonly httpService: HttpService,
// inject facades from other bff modules
private readonly mediaFacade: MediaFacade,
private readonly userFacade: UserFacade
) {
}
async handle(bookId: string): Promise<GetBookDetailsResponse> {
// fetch basic resource
const book = await this.getBookDetails(bookId);
// extract ids of additional resources
const userId = book.addedByUserId;
const mediaId = book.mediaId;
// fetch additional resources in parallel using Promise.all
const [user, image] = await Promise.all([
this.userFacade.getUserDetails(userId),
this.mediaFacade.getMediaDetails(mediaId)
]);
// combine all resources into response
return this.createResponse({ book, user, image });
}
...
}
Extras and anti-patterns
The bff can and perhaps should integrate these features:
- integration with chosen external services (e.g., authentication, captcha validation, internal error tracking),
- proxy functionality for requests that don't need aggregation,
- altering how authorization tokens are stored (e.g., while the bff – microservice communication passes the token in the request header, the client-bff channel can adopt the more secure http-only cookies)
- filtering or forwarding errors from microservices,
- logging requests and responses during http interactions with microservices (particularly useful in development),
- caching,
- versioning the client-bff api contract,
- request-retry logic,
- SSR (angular universal),
- various security measures (throttling, CORS, cross-site request forgery protection, rate limiting, etc.),
- mock response generation for the client (built upon bff and client-bff contracts).
Things to avoid with bff:
- implementing business logic (all business logic belongs on the microservices side; bff is not a microservice!),
- aggregating data-modifying requests (risk of inconsistencies),
- sharing a single bff across multiple clients (that reverts to a classic gateway),
- one client consuming several bffs,
- handling authentication (verifying access to an action or resource at the bff level).
Final thoughts
No universal solution exists for backend-to-frontend communication; each strategy carries its own trade-offs. The Backend for Frontend (by Frontend) pattern seems particularly well-suited when:
- more than one client consumes microservice APIs,
- the project team has spare frontend capacity to maintain an additional application on the FE side,
- there's a desire to streamline contracts with microservices while delivering tailored APIs for each client.
For frontend engineers, it's also a valuable chance to gain introductory experience with how server-side applications are built and operated.
