The Target Architecture — A Bird’s-Eye View

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.


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. A greenfield rewrite is a rare chance to shed those accumulated constraints and design an architecture where the components and boundaries align from the start.

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:

%%{init: {'theme': 'dark'}}%%
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/>&nbsp;&nbsp;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?

ContainerWhy it exists
Nginx ProxyEliminates 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 SPAThe heart of the system — renders HTML via SSR, runs the GraphQL gateway in-process, and serves the hydrated Vue 3 client application.
.NET APIOwns 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.
RedisCoordinates 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:

%%{init: {'theme': 'dark'}}%%
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 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 load-bearing infrastructure. Approximately 40–60% of the TypeScript code in the application is generated, not hand-written.

%%{init: {'theme': 'dark'}}%%
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

InputGeneratorGenerated OutputManual Work Eliminated
.graphql files + schemaGraphQL CodegenTypeScript types, rich text fragments, possibleTypes mapsTS interface authoring, cache misconfiguration, copy-paste fragments
.graphql filesGraphQL Toolkit ModuleVue composables (useXxxQuery, useXxxClient)REST client, mapper, error handling
YAML translation fileTyped i18n ModuleTyped t.section.key proxy chainRuntime key lookups, missing-key bugs
GraphQL schemaGraphQL CodegenComponent stubs + GraphQL queries (entry + collection per type)Boilerplate components, manual query writing
GraphQL input typesIntrospection FormsType-safe, validatable form components + field metadataManual field wiring, dual validation
YAML config files (per env)Config GeneratorPipeline variable files, Bicepparam files, ACA container manifestsManual env-specific file maintenance, drift between environments
Modules, pages, components + nuxt.config.tsNuxt BuildFull TS app scaffold, Vite config, auto-imports, lazy component wrappersManual imports, route-level code splitting, build config wiring

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 — 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:

%%{init: {'theme': 'dark'}}%%
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:

%%{init: {'theme': 'dark', 'themeVariables': { 'signalTextColor': '#888888', 'labelTextColor': '#888888', 'noteBkgColor': '#444444', 'noteTextColor': '#cccccc' }}}%%
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:

%%{init: {'theme': 'dark'}}%%
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:

MetricChange
Median response timeOrders of magnitude faster
Error rate under loadNearly eliminated
Max tested capacityMultiples higher
Lighthouse Performance (mobile)Near-perfect
Infrastructure costElastic instead of fixed

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.

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 makes the codebase navigable even as the module count grows into the dozens.

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.


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.

Categories: