The Target Architecture — A Bird’s-Eye View

Close-up of a modern building facade featuring smooth white curves and vertical lines. Hero image credit: Photo by REFARGOTOHP on Pexels

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.


Table of Contents

From Problem to Blueprint

The previous article exposed the problem: manual synchronization across rendering layers, fragile data contracts, and performance that no amount of hardware can fix. This article maps the complete target architecture end-to-end — every major component, why it exists, and which article dives deeper into it. 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<br/>(CDN + WAF + TLS)"] FD --> Proxy subgraph Cluster["Container Apps Environment / K8s Cluster"] direction TB Proxy["<b>Nginx Proxy</b><br/>─────────────<br/>• TLS termination<br/>• Image caching & optimization<br/>• Static asset serving<br/>• OpenTelemetry spans<br/>• Same-origin for all requests"] SPA["<b>Nuxt 4 SPA</b><br/>─────────────<br/>• Vue 3 SSR rendering<br/>• In-process Apollo Server (GraphQL gateway)<br/>• Schema stitching (CMS + .NET)<br/>• Multi-tier page cache<br/>• Code-generated composables<br/>• Dozens of custom modules"] API["<b>.NET API</b><br/>─────────────<br/>• Business logic (pricing, orders, validation)<br/>• GraphQL subgraph endpoint<br/>• Form validation rules (Dryv)<br/>• Service integrations<br/>• Offer engine, order processing,<br/>  payment gateway, CRM / SAP"] Redis["<b>Redis</b><br/>─────────────<br/>• Page-level response cache<br/>• Cache invalidation Pub/Sub<br/>• Per-environment isolation"] Proxy --> SPA SPA --> API SPA --> Redis API --> Redis end SPA --> CMS["Headless CMS"] SPA --> KV["Azure Key Vault"] API --> KV Proxy --> AI["Application Insights"] SPA --> AI API --> AI Redis --> AI

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, pricing calculations, and form validation rule definitions. Calls internal and external services (offer engine, order processing, payment, CRM). As a separate, non-internet-facing container it can be configured with stricter security policies.
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<br/>(renders HTML on server)"] end subgraph Gateway["In-Process Apollo Server"] Stitcher["Schema Stitcher<br/>(merges CMS + .NET schemas)"] Delegate["@delegate Directive<br/>(cross-subgraph resolution)"] Cache["Per-Operation Cache<br/>(LRU, TTL-based)"] end subgraph Modules["Dozens of Custom Modules"] M1["Data & API integration"] M2["Content & localization"] M3["UI & design system"] M4["Analytics & experimentation"] M5["Security & infrastructure"] M6["Developer experience"] end subgraph Nitro["Nitro Server"] MW["Server Middleware<br/>(auth, CSRF, CSP, caching)"] Handlers["API Handlers<br/>(/api/graphql, /api/health, etc.)"] end VueSSR -->|"function call<br/>(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 ClientApollo ServerSubgraph 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 load-bearing infrastructure. Approximately 40–60% of the TypeScript code in the application is generated, not hand-written.

flowchart LR subgraph Inputs["Developer Inputs"] Schema["GraphQL schema<br/>(introspected at build)"] GQL[".graphql query files"] YAML["YAML translation files"] InfraYAML["YAML config files<br/>(per environment)"] NuxtInput["Modules, pages, components<br/>+ nuxt.config.ts"] end subgraph Generator["Build-Time Generators"] GQLGEN["GraphQL Codegen<br/>(types + fragments)"] GQLT["GraphQL Toolkit Module<br/>(useXxxQuery, useXxxClient)"] I18NGEN["Typed i18n Module<br/>(typed translation proxy)"] FORMGEN["Introspection Forms<br/>(field metadata + validation)"] CFGGEN["Config Generator"] NUXT["Nuxt Build"] end subgraph Outputs["Generated Code"] Types["TypeScript types<br/>(every field, every nullable)"] Composables["Vue composables<br/>(useXxxQuery, useXxxClient)"] Translations["Typed t.section.key API"] Components["CMS component stubs"] CFQueries["GraphQL queries<br/>(entry + collection per type)"] Fragments["Rich text fragments"] PossibleTypes["Union/interface maps"] Forms["Type-safe validatable<br/>form component factories"] PipelineVars["YAML pipeline variable files"] BicepParams["Bicepparam files"] ACAManifests["ACA container manifests"] TSApp["Full TS app scaffold<br/>(Vite config, entry points)"] AutoImports["Auto-imports<br/>(composables, components, utils)"] LazyComponents["Lazy component wrappers<br/>(code-split per route)"] end Schema --> GQLGEN YAML --> I18NGEN Schema --> FORMGEN GQLGEN --> Types GQLGEN --> Fragments GQLGEN --> PossibleTypes GQL --> GQLT GQLT --> Composables I18NGEN --> Translations GQLGEN --> Components GQLGEN --> CFQueries FORMGEN --> Forms InfraYAML --> CFGGEN CFGGEN --> PipelineVars CFGGEN --> BicepParams CFGGEN --> ACAManifests NuxtInput --> NUXT NUXT --> TSApp NUXT --> AutoImports NUXT --> LazyComponents

What Gets Generated

Input Generator Generated Output Manual Work Eliminated
.graphql files + schema GraphQL Codegen TypeScript types, rich text fragments, possibleTypes maps TS interface authoring, cache misconfiguration, copy-paste fragments
.graphql files GraphQL Toolkit Module Vue composables (useXxxQuery, useXxxClient) REST client, mapper, error handling
YAML translation file Typed i18n Module Typed t.section.key proxy chain Runtime key lookups, missing-key bugs
GraphQL schema GraphQL Codegen Component stubs + GraphQL queries (entry + collection per type) Boilerplate components, manual query writing
GraphQL input types Introspection Forms Type-safe, validatable form components + field metadata Manual field wiring, dual validation
YAML config files (per env) Config Generator Pipeline variable files, Bicepparam files, ACA container manifests Manual env-specific file maintenance, drift between environments
Modules, pages, components + nuxt.config.ts Nuxt Build Full TS app scaffold, Vite config, auto-imports, lazy component wrappers Manual imports, route-level code splitting, build config wiring

The developer workflow becomes: write a declaration, run the code generator, 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 — Dozens of Boundaries

The application is not a monolith with folders. It is a composition of dozens of Nuxt modules, each owning a vertical slice of functionality. An enterprise website of this scale inevitably needs modules across several categories:

flowchart TB subgraph Core["Core Modules"] direction LR GQL["Data & API integration<br/>(GraphQL, schema stitching,<br/>CMS connectivity)"] UI["UI & design system<br/>(component library,<br/>layout primitives)"] FORMS["Forms & validation<br/>(schema-driven fields,<br/>cross-stack rules)"] end subgraph Content["Content & Localization"] direction LR L10N["Localization<br/>(typed translations,<br/>locale management)"] CACHE["Caching<br/>(multi-tier page cache,<br/>invalidation)"] AB["Experimentation<br/>(A/B testing,<br/>SSR-level variants)"] end subgraph Infra["Infrastructure & Security"] direction LR AUTH["Authentication & secrets<br/>(OAuth, key vault,<br/>token management)"] SEC["Security hardening<br/>(CSRF protection, CSP,<br/>request validation)"] OBS["Observability<br/>(structured logging,<br/>telemetry, tracing)"] end subgraph DX["Developer Experience"] direction LR DEVTOOLS["Custom DevTools<br/>(live inspection,<br/>CMS links, debug panels)"] TRACK["Analytics<br/>(event tracking,<br/>conversion monitoring)"] end NuxtApp["Nuxt 4 Application"] --> Core NuxtApp --> Content NuxtApp --> Infra NuxtApp --> DX

The exact number of modules depends on the project’s requirements, but enterprise applications commonly end up with dozens. 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

These are real architectural boundaries enforced by Nuxt’s module system. The categories above are illustrative; a real project will have modules for cookie consent, third-party widget integration, feature flags, payment flows, and many other concerns that only surface once the application reaches production scale.


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<br/>• Isolated per branch<br/>• Own Redis instance<br/>• Torn down on merge<br/>• Live URL for review"] end subgraph Test["Test"] T_DESC["• Stable integration<br/>• Blue-green deploys<br/>• Shared by QA<br/>• Production-like config"] end subgraph Prod["Production"] P_DESC["• Auto-scaled (5–20 replicas)<br/>• Blue-green deploys<br/>• Instant rollback<br/>• Full observability"] end end subgraph Config["Configuration System"] YAML["YAML values files<br/>(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 Result
Median response time 165 ms vs 2,618 ms — 15.9× faster
Error rate under load 0.09% vs 3.91% — 97% lower
Max tested capacity 494+ RPM vs ~99 RPM — 5× more
Lighthouse Performance (mobile) 97–100 — up 47 points
Infrastructure cost ~40% lower — elastic scaling vs fixed instances

All of these 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 the production code. The .graphql files are the source; the generated TypeScript is the implementation. This matters because generated types are always in sync with the schema — a renamed field in the backend becomes a TypeScript compiler error, not a undefined at runtime caught by a customer at 2 AM. Approximately 40–60% of the TypeScript codebase is generated, not hand-written.

4. Modules as Architectural Boundaries

Each module owns its composables, components, server handlers, and types. Cross-module communication happens through defined interfaces (hooks, event bus, shared state). This matters because folders are suggestions — nothing prevents a file in components/ from importing internals from services/checkout/. Nuxt modules enforce the boundary structurally: what is not exported is not accessible.

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 — the kind where a variable defined in the test manifest was never added to the production one, discovered at the worst possible moment.


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 — Typed placeholders and formatters for cross-subgraph queries.
  • Article 5: GraphQL-Based Code Generation — Eliminating All Boilerplate — How .graphql files 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.

Hero image credit: Photo by REFARGOTOHP on Pexels