Second in a series about migrating from legacy architectures to a modern Nuxt 4 stack. This article provides the architectural overview that frames every subsequent article in the series.
Why an Overview Matters
The previous article exposed the problem: manual synchronization across rendering layers, fragile data contracts, and performance that no amount of hardware can fix. The solution is not “use framework X” — it is a coherent architecture where every component exists for a reason and every boundary is deliberate.
This article walks through that architecture end-to-end. It introduces every major component, explains why it exists, and maps it to the detailed article that dives deeper. Think of it as the map before the journey.
The Four Containers
The entire application runs as four containers inside a managed container environment (Azure Container Apps or Kubernetes). Each container has a single, clear responsibility:
flowchart TB
Internet["Internet"] --> FD["Azure Front Door\n(CDN + WAF + TLS)"]
FD --> Proxy
subgraph Cluster["Container Apps Environment / K8s Cluster"]
direction TB
Proxy["Nginx Proxy\n─────────────\n• TLS termination\n• Image caching & optimization\n• Static asset serving\n• OpenTelemetry spans\n• Same-origin for all requests"]
SPA["Nuxt 4 SPA\n─────────────\n• Vue 3 SSR rendering\n• In-process Apollo Server (GraphQL gateway)\n• Schema stitching (CMS + .NET)\n• Multi-tier page cache\n• Code-generated composables\n• 35+ custom modules"]
API[".NET API\n─────────────\n• Business logic (pricing, orders, validation)\n• GraphQL subgraph endpoint\n• Form validation rules (Dryv)\n• Service integrations"]
Redis["Redis\n─────────────\n• Page-level response cache\n• Cache invalidation Pub/Sub\n• Session/state coordination\n• Per-environment isolation"]
Proxy --> SPA
SPA --> API
SPA --> Redis
end
SPA --> CMS["Headless CMS\n(Contentful)"]
SPA --> AI["Application Insights"]
API --> KV["Azure Key Vault"]
Why Four Containers?
| Container | Why it exists |
|---|---|
| Nginx Proxy | Eliminates cross-origin penalties, serves cached images and assets at the edge, adds distributed tracing spans, and keeps TLS configuration out of the application code. |
| Nuxt 4 SPA | The heart of the system — renders HTML via SSR, runs the GraphQL gateway in-process, and serves the hydrated Vue 3 client application. |
| .NET API | Owns business logic that cannot live in Node.js — transactional operations, legacy service integrations, pricing calculations, and form validation rule definitions. |
| Redis | Coordinates cache state across multiple SPA replicas and stores page-level response caches. Each environment (and each feature branch) gets its own Redis instance to prevent cross-contamination. |
The Nuxt 4 SPA — Where Most of the Magic Lives
The SPA container is far more than a frontend renderer. It is a full-stack TypeScript application powered by Nuxt 4’s Nitro server engine:
flowchart TB
subgraph NuxtProcess["Nuxt 4 Process (Node.js)"]
direction TB
subgraph SSR["Server-Side Rendering"]
VueSSR["Vue 3 Component Tree\n(renders HTML on server)"]
end
subgraph Gateway["In-Process Apollo Server"]
Stitcher["Schema Stitcher\n(merges CMS + .NET schemas)"]
Delegate["@delegate Directive\n(cross-subgraph resolution)"]
Cache["Per-Operation Cache\n(LRU, TTL-based)"]
end
subgraph Modules["35+ Custom Modules"]
M1["GraphQL (GQLT)"]
M2["i18n (I18NT)"]
M3["Forms (Introspect)"]
M4["Tracking (GTM)"]
M5["A/B Testing"]
M6["UIKit / Design System"]
M7["...and 29 more"]
end
subgraph Nitro["Nitro Server"]
MW["Server Middleware\n(auth, CSRF, CSP, caching)"]
Handlers["API Handlers\n(/api/graphql, /api/health, etc.)"]
end
VueSSR -->|"function call\n(zero network overhead)"| Gateway
Gateway --> Stitcher
Stitcher --> Delegate
Stitcher --> Cache
end
Gateway -->|"HTTP"| CMS["CMS Subgraph"]
Gateway -->|"HTTP"| DOTNET[".NET API Subgraph"]
Cache -->|"get/set"| Redis["Redis"]
The In-Process GraphQL Gateway
This is the single most important architectural decision: the Apollo Server that stitches together all data sources runs inside the same Node.js process** as the Nuxt application.
During SSR, when a Vue component needs data, the call path is:
Vue component → useAsyncData → Apollo Client → Apollo Server → Subgraph resolver
That entire chain is a function call — no HTTP, no serialization, no network latency. The GraphQL gateway is just another module in the Nuxt process. On the client side, after hydration, the same gateway is reachable via HTTP at /api/graphql, but the server-rendered page was already produced without a single network hop for data.
Schema Stitching In-Process
At startup, the gateway fetches the GraphQL schemas of all connected subgraphs (CMS, .NET API) and merges them into a single unified schema. The frontend writes one GraphQL query that can span data from any source:
query ProductPage($path: String!, $zip: String!) {
# Resolved from CMS subgraph
page(path: $path) {
title
heroImage { url }
body { json, links { ... } }
}
# Resolved from .NET API subgraph
offers(postalCode: $zip) {
name
monthlyPrice
features
}
}
One query. One response. One set of generated TypeScript types. The frontend never needs to know which backend produced which field.
The Code Generation Pipeline
Code generation is not a nice-to-have — it is load-bearing infrastructure. Approximately 40–60% of the TypeScript code in the application is generated, not hand-written.
flowchart LR
subgraph Inputs["Developer Inputs"]
GQL[".graphql query files"]
YAML["YAML translation files"]
Schema["GraphQL schema\n(introspected at build)"]
CMSModel["CMS content model"]
end
subgraph Generator["Build-Time Generators"]
GQLGEN["GraphQL Codegen\n(types + composables)"]
I18NGEN["I18NT Generator\n(typed translation proxy)"]
CFGEN["Contentful Generator\n(component stubs + queries)"]
FORMGEN["Form Introspection\n(field metadata + validation)"]
end
subgraph Outputs["Generated Code"]
Types["TypeScript types\n(every field, every nullable)"]
Composables["Vue composables\n(useXxxQuery, useXxxClient)"]
Translations["Typed t.section.key API"]
Components["CMS component stubs"]
Fragments["Rich text fragments"]
PossibleTypes["Union/interface maps"]
end
GQL --> GQLGEN
Schema --> GQLGEN
YAML --> I18NGEN
CMSModel --> CFGEN
Schema --> FORMGEN
GQLGEN --> Types
GQLGEN --> Composables
GQLGEN --> Fragments
GQLGEN --> PossibleTypes
I18NGEN --> Translations
CFGEN --> Components
FORMGEN --> Composables
What Gets Generated
| Input | Generated Output | Manual Work Eliminated |
|---|---|---|
.graphql file | Typed composable + document node | REST client, TS interface, mapper, error handling |
| YAML translation file | Typed t.section.key proxy chain | Runtime key lookups, missing-key bugs |
| CMS content model | Component stubs + typed queries | Boilerplate components, manual query writing |
| GraphQL input types | Form field metadata + validation rules | Manual field wiring, dual validation |
| Subgraph schemas | possibleTypes maps + rich text fragments | Cache misconfiguration, copy-paste fragments |
The developer workflow becomes: write a declaration, run yarn gen, get a fully typed implementation. The TypeScript compiler then validates everything against the schema — a renamed field produces a compile error, not a production bug.
The Module System — 35+ Boundaries
The application is not a monolith with folders. It is a composition of 35+ Nuxt modules, each owning a vertical slice of functionality:
flowchart TB
subgraph Core["Core Modules"]
direction LR
GQL["graphql\n(Apollo, stitching)"]
I18N["i18n\n(translations)"]
UIKIT["uikit\n(design system)"]
FORMS["introspect-forms\n(schema-driven forms)"]
end
subgraph Data["Data & Content Modules"]
direction LR
CF["contentful\n(CMS integration)"]
CACHE["page-cache\n(Redis + LRU)"]
AB["ab-tests\n(SSR-level variants)"]
end
subgraph Infra["Infrastructure Modules"]
direction LR
AUTH["azure-auth\n(OAuth 2.0)"]
KV["azure-keyvault\n(secret loading)"]
LOG["logging\n(App Insights)"]
SEC["security\n(CSRF, CSP)"]
end
subgraph DX["Developer Experience Modules"]
direction LR
DEVTOOLS["dev-tools-data\n(custom DevTools)"]
CFDEV["contentful-devtools\n(CMS links)"]
DEBUG["debug-panel\n(live inspection)"]
end
NuxtApp["Nuxt 4 Application"] --> Core
NuxtApp --> Data
NuxtApp --> Infra
NuxtApp --> DX
Each module:
- Has a clear public API (exported composables, components)
- Owns its server middleware, plugins, and handlers
- Is independently documentable (each has a README)
- Can be added or removed without ripple effects across the codebase
This is not folder organization — these are real architectural boundaries enforced by Nuxt’s module system.
The Request Lifecycle
Here is what happens when a user requests a page, end-to-end:
sequenceDiagram
participant U as User Browser
participant FD as Azure Front Door
participant P as Nginx Proxy
participant N as Nuxt 4 (SSR)
participant GW as Apollo Gateway (in-process)
participant CMS as CMS Subgraph
participant API as .NET API
participant R as Redis
U->>FD: GET /strom/tarife
FD->>P: Forward (CDN miss)
P->>N: Proxy to Nuxt
Note over N: SSR begins
N->>N: Evaluate A/B tests, conditions
N->>GW: GraphQL query (function call)
Note over GW: Check page data cache
GW->>R: GET cached CMS data for path
alt Cache hit (CMS data)
R-->>GW: Cached CMS entries
else Cache miss
GW->>CMS: Resolve content fields
CMS-->>GW: Page content
GW->>R: Cache CMS data for path
end
GW->>API: Resolve business fields (real-time)
API-->>GW: Pricing data
GW-->>N: Unified response
N->>N: Render HTML (uses A/B variant + data)
N-->>P: 200 HTML + hydration payload
P-->>FD: Response
FD-->>U: HTML (visible immediately)
Note over U: Hydration
U->>U: Vue attaches to DOM
U->>U: Page is interactive
Key points:
- Zero network overhead for SSR data: The GraphQL gateway is in-process
- Data caching, not HTML caching: CMS content entries are cached per path in Redis; HTML is rendered fresh every
request because A/B tests, user-specific conditions, and other factors may differ
- Multi-tier caching: Redis → in-memory LRU → per-operation cache (CMS data only; business data stays real-time)
- Single roundtrip: One GraphQL query fetches all data for a page
- Instant interactivity: Hydration attaches to existing DOM, no re-render
The Infrastructure Model
The container environment scales elastically and supports three deployment tiers:
flowchart TB
subgraph Environments["Deployment Environments"]
direction LR
subgraph Feature["Feature Branches"]
F_DESC["• Provisioned on git push\n• Isolated per branch\n• Own Redis instance\n• Torn down on merge\n• Live URL for review"]
end
subgraph Test["Test"]
T_DESC["• Stable integration\n• Blue-green deploys\n• Shared by QA\n• Production-like config"]
end
subgraph Prod["Production"]
P_DESC["• Auto-scaled (5–20 replicas)\n• Blue-green deploys\n• Instant rollback\n• Full observability"]
end
end
subgraph Config["Configuration System"]
YAML["YAML values files\n(single source of truth)"]
GEN["Generator"]
MAN["Container manifests"]
BICEP["Bicep params"]
VARS["Pipeline variables"]
YAML --> GEN
GEN --> MAN
GEN --> BICEP
GEN --> VARS
end
Everything is generated from YAML configuration files. Adding a new environment variable means defining it once — the generator produces the correct manifests, parameters, and pipeline variables for every environment and platform combination.
What This Architecture Delivers — A Preview
Before diving into the design rationale, here is what the combined effect of these decisions looks like under real production load:
| Metric | Change |
|---|---|
| Median response time | Orders of magnitude faster |
| Error rate under load | Nearly eliminated |
| Max tested capacity | Multiples higher |
| Lighthouse Performance (mobile) | Near-perfect |
| Infrastructure cost | Elastic instead of fixed |
These are not projections — they are measured results from load testing against production-equivalent traffic patterns. Article 19 covers the full methodology and breakdown. The rest of this series explains how and why each architectural layer contributes to these numbers.
The Key Architectural Decisions
Five decisions define this architecture. Every other choice flows from them:
1. SSR with In-Process Gateway
The GraphQL gateway runs inside Nuxt, not as a separate service. This eliminates an entire network hop during server rendering and reduces infrastructure complexity from five services to four containers.
2. Schema Stitching Over Federation
Schema stitching works with any GraphQL endpoint — including third-party CMS APIs you cannot modify. Federation requires control over all subgraphs. Stitching was the only option that fit the real-world constraints.
3. Code Generation as Load-Bearing Infrastructure
Generated code is not scaffolding you edit later. It is the production code. The .graphql files are the source; the generated TypeScript is the implementation. This inverts the usual relationship between developers and boilerplate.
4. Modules as Architectural Boundaries
Not folders, not conventions — modules. Each module owns its composables, components, server handlers, and types. Cross-module communication happens through defined interfaces (hooks, event bus, shared state). This makes the codebase navigable at 35+ modules and growing.
5. Configuration Generation Over Management
Infrastructure configuration is not managed by hand. YAML files define the truth; a generator produces Container Apps manifests, Bicep parameters, and pipeline variables. This eliminates an entire class of deployment bugs.
Lessons Learned
Start with the container topology
Before writing any application code, decide how many containers you need and what each one owns. The four-container model (proxy, SPA, API, cache) provides clean separation without over-engineering. Each container can scale independently and be deployed independently.
The gateway position matters more than you think
Running the GraphQL gateway in-process versus as a sidecar changes the performance characteristics of every single page render. In-process means zero-latency data fetching during SSR. A sidecar would add 5–20ms per query — multiplied by every page render, every second.
Code generation needs a schema to generate from
The entire automation story depends on having authoritative schemas: GraphQL for data, YAML for translations, CMS models for content. Without schemas, there is nothing to generate from. Invest in schema quality — it pays compound interest.
An architecture overview prevents tunnel vision
Teams that understand the big picture make better local decisions. When a developer knows that the proxy handles image caching, they do not build image optimization into the SPA. When they know Redis coordinates cache invalidation, they do not invent ad-hoc signaling. The overview is not documentation for its own sake — it is a decision-making framework.
What’s Next
With the architecture mapped out, the following articles dive into each component in detail:
- Article 3: GraphQL Schema Stitching — One API to Rule Them All — How the in-process gateway merges multiple data
sources into a single schema.
- Article 4: The @delegate Directive Deep Dive — Cross-subgraph field resolution with typed placeholders and
formatters.
- Article 5: GraphQL-Based Code Generation — Eliminating All Boilerplate — How
.graphqlfiles become fully typed
Vue composables.
Each article builds on the mental model established here. When you encounter “the gateway resolves this field from the CMS subgraph,” you will know exactly where that gateway lives, how it connects, and why the design works the way it does.
Munir Husseini is a software architect specializing in full-stack TypeScript, .NET, and cloud-native architectures.





