Every React plus .NET programme eventually hits the same wall: the SPA talks to four microservices, stores tokens in browser storage, and implements retry logic in three hooks that disagree. The fix is not “use React Query harder.” Draw the BFF boundary once: server-side auth, aggregation, and cache ownership on ASP.NET; client-side rendering and interaction state in React. Enforce that split with a decision table your team can cite in code review.
Integration tax without a BFF
Count the edges on a typical operations dashboard: React app, four REST APIs, an OAuth identity provider, a CDN for assets, and often a WebSocket feed. Without a BFF, the browser holds six integration surfaces. Each surface carries engineering cost that compounds:
- OAuth in the browser — refresh tokens exposed to XSS; PKCE helps public clients but does not remove token storage risk.
- CORS matrix — every new API origin requires preflight configuration and security review.
- Aggregation in hooks —
useEffectchains fan out N requests per page; slowest call sets perceived latency. - Duplicated error shapes — each service returns different problem JSON; the UI invents adapters per screen.
- Cache inconsistency — React Query caches per key in the browser; no shared invalidation when a backend mutation occurs on another service.
- Secret leakage — developers embed API keys in frontend env vars “temporarily.”
- Version skew — mobile web and desktop web pin different API versions because there is no single choke point.
Microsoft’s Backends for Frontends pattern describes the intent: one backend per user experience, shaped for that UI. The IETF draft on OAuth 2.0 for Browser-Based Apps reinforces what security reviewers already know: confidential tokens belong server-side.
Decision table: who owns what
| Concern | React SPA owns | .NET BFF owns | Anti-pattern |
|---|---|---|---|
| Access token | Never stores refresh token | HttpOnly session cookie or server-side token cache | localStorage refresh token |
| Dashboard data | Render skeletons, optimistic UI | Fan-out, parallel downstream calls, single response DTO | Four hooks each calling a microservice URL |
| Cache | UI state, form drafts | Redis cache-aside on aggregated read models | Browser cache of half-stale cross-service joins |
| Authorisation | Hide buttons based on claims in BFF payload | Policy evaluation, scope checks before downstream calls | Trust client-side role strings |
| Errors | Display normalised ProblemDetails |
Map downstream failures to stable codes | Expose raw 502 bodies from internal services |
| File upload | Chunked UX progress | Virus scan, storage, metadata persist | Presigned URL logic duplicated per feature |
| Real-time updates | Subscribe via BFF WebSocket or SSE | Bridge from message bus to connection groups | Browser subscribed directly to RabbitMQ (yes, we have seen it) |
Boundary rules that survive hiring
Three rules BlackFlow puts in architecture review for React plus .NET programmes:
- No microservice base URL in React env except the BFF. If
VITE_ORDERS_APIexists, the boundary was lost. - Aggregates are named after screens, not tables.
GET /bff/dashboard/operations-summaryreturns one DTO; the BFF joins orders, inventory, and alerts internally. - Mutations go through the BFF command path. The BFF validates, calls the owning service, and returns the same error envelope whether the failure was auth, validation, or downstream timeout.
React keeps component state, accessibility, and client-side routing. It does not own cross-service orchestration or token lifecycle. That split reduces the edge count from six browser integrations to one, plus the BFF’s server-side integrations which are easier to secure, observe, and cache.
Acceptance protocol (sandbox)
Before signing off a BFF layer, run these steps in a staging VLAN any engineer can repeat:
- Load the dashboard with DevTools network tab open — exactly one BFF request (or one SSE connection) should satisfy the primary view.
- Revoke a downstream API credential server-side — the BFF returns partial data with explicit
degraded: truefields, not a blank screen or stack trace. - Expire the session cookie — React redirects to login without ever having held a refresh token in JS-accessible storage.
- Invalidate a Redis cache key on the BFF — all users see fresh aggregated data within the stated TTL without clearing browser storage.
- Add a mock downstream latency of 3 s on one dependency — BFF timeout policy returns within 5 s with a stable timeout error code, not a hung UI.
Failure on any step means the boundary was drawn on a diagram but not in deployment configuration.
Team ownership and multiple BFFs
One BFF does not mean one giant Program.cs. Large programmes split BFFs by user experience, not by microservice: Operations.Bff and Partner.Bff can share authentication middleware and Redis clusters while exposing different aggregates. React apps map one-to-one to their BFF base URL. Shared libraries hold DTO mappers; duplicated OAuth wiring is the smell that the split went wrong.
Frontend engineers own component tests and contract tests against BFF OpenAPI exports. Backend engineers own fan-out timeouts, cache keys, and downstream circuit breakers. That split ends the recurring meeting about “whether this fetch belongs in the hook.” If the data needs joining, it belongs in the BFF by default; exceptions require a line in the decision table with a named owner.
When custom BFF logic still beats a generic gateway
API gateways solve TLS termination, rate limits, and routing. They do not know that the operations dashboard needs orders and alerts joined on facilityId. A .NET BFF is custom code because aggregation rules are product rules. Buy a gateway for cross-cutting ingress; build a BFF for experience-shaped contracts.
Programmes with fewer than three downstream reads and no auth complexity may keep a thin React-to-API path temporarily. The decision table above is the tripwire: when the anti-pattern column starts filling, schedule the BFF slice before the next security audit.
Security review checklist item: search the React repo for localStorage.setItem and sessionStorage touching tokens. Zero hits is the pass condition for BFF-backed auth. Any hit triggers migration to cookie-based session or BFF-issued opaque session IDs.
What BlackFlow takes from this pattern
React skills and .NET skills in the same team do not automatically produce a clean boundary. The boundary is a deliberate ownership map: browser renders, server integrates. Draw it once, test it with the acceptance protocol, and stop debating token storage in every sprint.
Shipping React frontends on ASP.NET BFFs with Redis and RabbitMQ behind the facade? Talk to BlackFlow about custom software where the integration tax is paid server-side, not in every hook.

