A question that comes up on almost every microservices project sooner or later is whether the UI should call each backend API directly, or whether there should be a gateway service sitting in between. This is not really a technology decision, it is an architecture and security decision, and it is worth thinking through properly before wiring up API Management, YARP, or Ocelot just because it is the fashionable thing to do.
Setup With the UI Calling APIs Directly
Most public facing applications protect their APIs using user delegated access tokens, and when the UI calls each API directly, every one of those APIs sits in the public zone. The SPA or client application merges the data from multiple APIs itself. Each API can carry its own scope, but that also means the client ends up managing multiple access tokens, and every API needs to allow CORS if the client is a browser based SPA.

Server rendered applications sidestep the CORS problem since the browser is not calling the APIs directly, but the core characteristic of this setup does not change: the UI is responsible for joining data across API calls for whatever view it needs to render, and you can end up with multiple UIs per module or per microservice, each carrying that same joining logic.
Setup With a Gateway in Front (BFF or API Gateway)
Adding a gateway service does not remove the need for HTTPS anywhere in the chain, the gateway should not terminate HTTPS and hand off to plain HTTP behind it, not even in development. Every service still needs to apply its own security headers at full strength.
Where a gateway earns its place is in optimizing the API surface for the UI. Most microservices APIs are built to a domain model, not a screen, and calling them directly from the UI usually means extra round trips and logic in the client to reshape the responses. A gateway can absorb that reshaping, so the UI ends up calling fewer, better shaped endpoints.
One thing worth being careful about: do not use the gateway to paper over missing security headers on APIs you own. Fix those at the source. The one place a gateway earns its keep for security headers is a legacy API you cannot modify directly, there patching at the gateway is a reasonable stopgap.

Run the gateway as a reverse proxy and the backend APIs never need to be exposed to the internet at all. Fewer services in the public zone means a smaller attack surface, and less is genuinely more here.
Where the Gateway Actually Improves Security
The public facing gateway can keep using the user delegated access token, or a session cookie if you are running a BFF style architecture. Everything behind the gateway can switch to application level trust: OAuth client credentials flow, certificate authentication, or managed identities if you are on Azure. The one rule that matters here is that a public user access token must never work against a private API. Keep those trust boundaries separate.
This split also makes testing considerably easier. System tests against APIs that require user delegated tokens are painful, since a proper token needs actual user interaction through a UI test tool, and building a back door to skip that is a bad habit that tends to leak into production. Once the private APIs sit behind the gateway using application level trust, they become straightforward to exercise with system tests.
CORS also gets simpler. Only the public gateway needs to allow CORS for an SPA client, every private API behind it can disable CORS entirely and apply the full set of security headers without compromise. If you are using cookies, same site and same domain restrictions can be enforced at the single gateway and UI pairing rather than across every backend service.
User authorization logic also tends to consolidate at the gateway and UI layer, leaving private APIs to validate trusted app to app requests rather than full user authorization, though this is not a hard rule, patterns like on-behalf-of flows for the private APIs are equally valid depending on what the system needs.
The Trade-off Nobody Skips Around
None of this comes free. A gateway is an extra hop, an extra thing to deploy, monitor, and keep highly available, and it becomes a single point of failure for everything behind it if you do not plan for redundancy. For a small system with two or three backend services and a single UI, that extra operational surface can easily cost more than the complexity it removes from the client.
KISS still wins more often than architecture diagrams suggest. Plenty of systems that look like they need microservices would be simpler, cheaper to run, and just as capable as a single well structured monolith. If you do go the microservices route, do it with a real security architecture in mind, define your public and private zones deliberately, and do not hesitate to merge services back together if you find two of them are tightly coupled and always deployed and changed together anyway. A gateway is a tool for a specific problem, not a default you reach for on every project.
Leave a Reply