The following article presents our approach to designing, maintaining and consuming APIs (from a frontend developer’s perspective) and the solution that we have implemented in one of our projects, which has significantly impacted the communication with backend.
SPA applications developed with Angular are foremost consumers for dedicated APIs. They provide graphical interfaces for end users, designed to interact with server applications.
API itself can be either general-purpose (targeted toward many different consumers without any knowledge about them) or customized for a specific consumer. On the one hand we have the standard REST at our disposal, on the other hand we sometimes want to bend the rules a little bit to match a specific business requirement better. On the backend side, we want to keep things as simple as possible, eliminate coupling between microservices but on the frontend side we are looking for solutions that are optimal in terms of network traffic, minimizing the number of requests (and their processing time). Striking the right balance is not always easy and requires experience in API design.
As an Angular Team, we are always actively involved in the API design process. We focus on the final user experience, developer experience and the aspiration to create and maintain the best possible system architecture (in every sense of the word).
BFF(bF) case study
For one of the projects, as a frontend team, we decided to implement the Backend for Frontend pattern (a special variant of the API Gateway pattern). We gave it a draft name: Backend for Frontend by Frontend. We had following reasons for this decision:
- the aim to take some tasks off of the backend developer’s shoulders,
- the aim to separate the nasty data aggregation logic from the frontend application,
- introduction of a more secure way of handling authentication (using http-only cookies)
- having a customized API for each client in the system without creating many additional endpoints on the microservices side.
In this project, the server part is divided into microservices and in addition we are using many 3rd party services. We currently have two Angular applications and we plan to add more in the future. In case of scaling the system or the team, we can easily divide the developers into teams for each frontend application.
How does this architecture affect the API design process? The gape (field for negotiation) between frontend and backend teams is now between microservices and BFFs. At the BFF level, we can aggregate responses from multiple microservices and put them together into a combined response for an Angular application. Processing one request from the frontend to the BFF and N requests between the BFF and the backend is more efficientl than N requests between the Frontend and the Backend (assuming BFF is a part of the same local network as for example the microservices).
None of the core applications (neither frontend nor microservices) are “overloaded” with the aggregation logic and there is no need for the frontend app to wait for all partial resources. At the same time each customer has a unique and customized API that fully meets its needs.
Additional benefits gained from the implementation of BFFbF:
- the FE-BFF authentication based on http secure cookies (limiting the possibility of browser-based token capture attacks)
- integration with some external services bypassing the Backend (e.g. captcha, authorization, feature flag management tool are fully handled by BFF)
- API versioning for a specific client (especially important for mobile applications, where users are reluctant to download new versions),
- caching aggregated responses,
- the ability to completely isolate microservices from public access (only BFFs can access them, the rest of the services can be hidden, e.g., using a VPN),
- the ability to mock up responses from the server at the BFF level
The solution also has its disadvantages. These include:
- bigger workload for the frontend team
- predicted duplication of code and functionalities BFFs,
- additional DevOPs workload (need to maintain additional server applications, CI/CD, etc.)
- more attack vectors (while for a classic gateway we only have one point of contact between microservices and the outside world, here each BFF can be a separate attack vector)
How did we combine frontend applications and BFFs in practice? We took advantage of the monorepo strategy, using the nx workspace tool. Inside the repository, we placed both Angular applications (along with shared logic libraries and UI components library), BFFs (as applications created with the NestJS framework, with shared code libraries) and API contracts created as Typescript models. The presented technology stack (called NAN) allows us to maximize the potential benefits of a single repository and a single programming language.
Such architecture also allowed us to implement a very convenient mocking system (invaluable at times when we develop functionality for which we don’t yet have a backend API ready or when API is simply temporarily unavailable for some reason). The BFF has access to all requests sent by the frontend application, knows what the responses to them should be (based on contracts), and can easily serve mocks in their place.
When designing architecture, there are no silver bullets – it all depends on the context. As a frontend team, we have a lot of experience in dealing with different APIs and we choose the best possible solutions for each specific problem. So in what cases is the BFF pattern worth considering?
- when we have a system with several clients for microservice APIs,
- when we are keen on a high level API alignment with front-end applications while maintaining a healthy division of responsibilities on the microservices side and minimizing coupling,
- when the frontend team has sufficient resources to create and maintain an additional application (it requires additional time and at least basic knowledge of how server applications work).