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.


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?

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, legacy service integrations, pricing calculations, and form validation rule definitions.
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:

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

InputGenerated OutputManual Work Eliminated
.graphql fileTyped composable + document nodeREST client, TS interface, mapper, error handling
YAML translation fileTyped t.section.key proxy chainRuntime key lookups, missing-key bugs
CMS content modelComponent stubs + typed queriesBoilerplate components, manual query writing
GraphQL input typesForm field metadata + validation rulesManual field wiring, dual validation
Subgraph schemaspossibleTypes maps + rich text fragmentsCache 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:

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

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