feat(spa): proxy /api in dev-server, relative bffApiBaseUrl #259

Merged
julien merged 1 commits from feat/spa-dev-proxy into main 2026-06-01 11:39:40 +02:00
Owner

Summary

Make the SPAs reach the BFF as a same-origin call via an Angular dev-server /api proxy. Solves the "Backend unreachable" error surfaced during the ADR-0030 dockerised dev mode VM validation when the SPA is accessed from a remote browser (e.g. http://<vm-ip>:4200/), and bypasses CORS in dev altogether. Follow-up to the just-merged ADR-0030 implementation.

Root cause it fixes

Before this PR, both SPAs hardcoded bffApiBaseUrl: 'http://localhost:3000/api' (per ADR-0018) and the BFF's CORS_ALLOWED_ORIGINS only allowed http://localhost:4200,http://localhost:4300. Both assumptions hold for native nx serve (developer on the same machine as the BFF) but break the moment the browser sits on a different host than the BFF — exactly the case for the ADR-0030 apps Compose profile: open http://<vm-ip>:4200/ from your workstation and the SPA's localhost:3000 call hits your workstation's loopback (nothing there), not the VM's BFF. Even if the URL were right, the origin http://<vm-ip>:4200 is not in the CORS allowlist.

Fix

Switch to a same-origin dev pattern: the Angular dev-server proxies /api/* to the BFF, and environment.ts uses a relative '/api' URL.

File Change
apps/portal-shell/proxy.conf.js New. Maps /api → ${BFF_TARGET:-http://localhost:3000} (JS form so the env var can swap the target at startup).
apps/portal-admin/proxy.conf.js New. Same shape.
apps/portal-shell/project.json, apps/portal-admin/project.json serve.options.proxyConfig points at the new file.
apps/portal-shell/src/environments/environment.ts, apps/portal-admin/src/environments/environment.ts bffApiBaseUrl: 'http://localhost:3000/api''/api'. The comment block explains the rationale + how production siblings can still use an absolute origin if SPA + BFF live on different hosts.
apps/portal-shell/src/observability/tracing.ts new URL(environment.bffApiBaseUrl) would throw on a relative URL — resolved against window.location.origin so both relative (dev) and absolute (prod cross-origin) bases work.
infra/local/dev.compose.yml BFF_TARGET=http://portal-bff:3000 added to portal-shell and portal-admin so the proxy hits the BFF container by name (Compose DNS) when the apps profile is up. Native nx serve leaves the var unset and falls back to localhost:3000.

Why a relative URL is safe

  • ${bffApiBaseUrl}/health etc. compose to /api/health — relative paths work in fetch / HttpClient.
  • tracing.ts propagates traceparent on requests whose origin matches the BFF origin. Resolving the relative base against window.location.origin gives the current page's origin, which is exactly the origin the dev-server proxy serves from — so the regex still matches the right requests in dev. In a future cross-origin production deployment, environment.prod.ts can set an absolute bffApiBaseUrl; the URL constructor's second arg is ignored when the first is absolute, so the same code path keeps working.
  • Auth flow (feature-auth / auth.config.ts): consumes bffApiBaseUrl via DI as a string prefix — agnostic to absolute vs relative.

Scope notes

  • The OTel HTTP exporter (environment.otlpEndpoint = 'http://localhost:4318/v1/traces') and the cross-SPA links (adminAppUrl, shellAppUrl) remain absolute. They hit the same remote-browser problem on apps-profile access, but neither is blocking the user-visible "Backend unreachable" path this PR targets. Out of scope here; a follow-up could either proxy them too or surface them via runtime config.
  • This pattern is dev-server only — production builds do not use the proxy. Per-environment bffApiBaseUrl overrides remain the supported lever (ADR-0018), unchanged.

Test plan

  • docker compose -f dev.compose.yml --profile apps config still validates.
  • On the VM, ./infra/local/dev.sh up apps, then in the workstation browser open http://<vm-ip>:4200/ → SPA loads, the "Backend unreachable" message is gone, network tab shows /api/... calls succeeding (same-origin, no CORS preflight).
  • Native nx serve portal-shell still works (the proxy falls back to localhost:3000).
  • Trace headers (traceparent) appear on /api/* fetches in the browser network tab.
  • pnpm exec nx affected -t test build green on the two SPAs.

Related

  • ADR-0030 — the dockerised dev mode this enables to actually work from a remote browser.
  • ADR-0018 — the environment.ts per-env strategy this complements (does not supersede — production behaviour unchanged).
## Summary Make the SPAs reach the BFF as a **same-origin** call via an Angular dev-server `/api` proxy. Solves the "Backend unreachable" error surfaced during the ADR-0030 dockerised dev mode VM validation when the SPA is accessed from a remote browser (e.g. `http://<vm-ip>:4200/`), and bypasses CORS in dev altogether. Follow-up to the just-merged ADR-0030 implementation. ## Root cause it fixes Before this PR, both SPAs hardcoded `bffApiBaseUrl: 'http://localhost:3000/api'` (per ADR-0018) and the BFF's `CORS_ALLOWED_ORIGINS` only allowed `http://localhost:4200,http://localhost:4300`. Both assumptions hold for native `nx serve` (developer on the same machine as the BFF) but break the moment the browser sits on a different host than the BFF — exactly the case for the ADR-0030 `apps` Compose profile: open `http://<vm-ip>:4200/` from your workstation and the SPA's `localhost:3000` call hits your **workstation's** loopback (nothing there), not the VM's BFF. Even if the URL were right, the origin `http://<vm-ip>:4200` is not in the CORS allowlist. ## Fix Switch to a same-origin dev pattern: the Angular dev-server proxies `/api/*` to the BFF, and `environment.ts` uses a relative `'/api'` URL. | File | Change | | --- | --- | | `apps/portal-shell/proxy.conf.js` | **New.** Maps `/api → ${BFF_TARGET:-http://localhost:3000}` (JS form so the env var can swap the target at startup). | | `apps/portal-admin/proxy.conf.js` | **New.** Same shape. | | `apps/portal-shell/project.json`, `apps/portal-admin/project.json` | `serve.options.proxyConfig` points at the new file. | | `apps/portal-shell/src/environments/environment.ts`, `apps/portal-admin/src/environments/environment.ts` | `bffApiBaseUrl: 'http://localhost:3000/api'` → `'/api'`. The comment block explains the rationale + how production siblings can still use an absolute origin if SPA + BFF live on different hosts. | | `apps/portal-shell/src/observability/tracing.ts` | `new URL(environment.bffApiBaseUrl)` would throw on a relative URL — resolved against `window.location.origin` so both relative (dev) and absolute (prod cross-origin) bases work. | | `infra/local/dev.compose.yml` | `BFF_TARGET=http://portal-bff:3000` added to `portal-shell` and `portal-admin` so the proxy hits the BFF **container** by name (Compose DNS) when the `apps` profile is up. Native `nx serve` leaves the var unset and falls back to `localhost:3000`. | ## Why a relative URL is safe - `${bffApiBaseUrl}/health` etc. compose to `/api/health` — relative paths work in `fetch` / `HttpClient`. - `tracing.ts` propagates `traceparent` on requests whose origin matches the BFF origin. Resolving the relative base against `window.location.origin` gives the current page's origin, which is exactly the origin the dev-server proxy serves from — so the regex still matches the right requests in dev. In a future cross-origin production deployment, `environment.prod.ts` can set an absolute `bffApiBaseUrl`; the URL constructor's second arg is ignored when the first is absolute, so the same code path keeps working. - Auth flow (`feature-auth` / `auth.config.ts`): consumes `bffApiBaseUrl` via DI as a string prefix — agnostic to absolute vs relative. ## Scope notes - The OTel HTTP exporter (`environment.otlpEndpoint = 'http://localhost:4318/v1/traces'`) and the cross-SPA links (`adminAppUrl`, `shellAppUrl`) **remain absolute**. They hit the same remote-browser problem on `apps`-profile access, but neither is blocking the user-visible "Backend unreachable" path this PR targets. Out of scope here; a follow-up could either proxy them too or surface them via runtime config. - This pattern is dev-server only — production builds do not use the proxy. Per-environment `bffApiBaseUrl` overrides remain the supported lever (ADR-0018), unchanged. ## Test plan - [x] `docker compose -f dev.compose.yml --profile apps config` still validates. - [ ] **On the VM**, `./infra/local/dev.sh up apps`, then in the workstation browser open `http://<vm-ip>:4200/` → SPA loads, the "Backend unreachable" message is gone, network tab shows `/api/...` calls succeeding (same-origin, no CORS preflight). - [ ] Native `nx serve portal-shell` still works (the proxy falls back to `localhost:3000`). - [ ] Trace headers (`traceparent`) appear on `/api/*` fetches in the browser network tab. - [ ] `pnpm exec nx affected -t test build` green on the two SPAs. ## Related - [ADR-0030](docs/decisions/0030-dockerised-dev-mode.md) — the dockerised dev mode this enables to actually work from a remote browser. - [ADR-0018](docs/decisions/0018-environment-configuration-strategy.md) — the `environment.ts` per-env strategy this complements (does not supersede — production behaviour unchanged).
julien added 1 commit 2026-06-01 11:21:13 +02:00
feat(spa): proxy /api in dev-server, relative bffApiBaseUrl
CI / check (pull_request) Successful in 4m8s
CI / commits (pull_request) Successful in 4m12s
CI / scan (pull_request) Successful in 5m6s
CI / a11y (pull_request) Successful in 1m47s
CI / perf (pull_request) Successful in 6m21s
8dd235e44f
Make the SPAs reach the BFF same-origin via the Angular dev-server's
proxy, so the dockerised dev mode (ADR-0030) works when the SPA is
accessed from a remote browser (e.g. http://<vm-ip>:4200/) — and CORS
is bypassed in dev altogether.

- New proxy.conf.js per SPA: /api -> ${BFF_TARGET:-http://localhost:3000}
  (JS form so the env var can swap the target at startup without a
  rebuild).
- project.json serve.options.proxyConfig wired in for both apps.
- environment.ts (shell + admin) bffApiBaseUrl: 'http://localhost:3000/api'
  -> '/api'. Production siblings can still set an absolute origin if
  the SPA and BFF live on different hosts; tracing.ts resolves either
  form against window.location.origin.
- tracing.ts: new URL(env, window.location.origin) — relative bases no
  longer throw, absolute bases keep their own origin.
- dev.compose.yml: BFF_TARGET=http://portal-bff:3000 on portal-shell
  and portal-admin so the proxy hits the BFF container by Compose DNS.
  Native nx serve leaves it unset and falls back to localhost.

OTel exporter URL and cross-SPA links remain absolute — same remote-
browser issue, but not blocking the 'Backend unreachable' path this
PR targets. Out of scope here, follow-up if needed.

Stacked on top of feat/dockerised-dev-mode.
julien merged commit a84ea2d116 into main 2026-06-01 11:39:40 +02:00
julien deleted branch feat/spa-dev-proxy 2026-06-01 11:39:41 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: julien/apf_portal#259