Skip to content

Operating Modes

Every full-stack Python + JavaScript application must answer four architectural questions:

  1. Who owns the initial HTML shell? (Vite index.html, Jinja2 template, or an external framework server?)
  2. Who owns URL routing? (Client router, Litestar route handlers, or a meta-framework router?)
  3. How are Vite <script> and <link> tags injected? (HTML transform pipeline, Jinja template helpers, or framework middleware?)
  4. How does data cross from Python to the UI? (Typed OpenAPI JSON, Inertia PageProps, or HTML fragments?)

ViteConfig(mode=...) answers all four questions with a single setting. Use the interactive diagram below to compare the primary operating modes:

SPA Mode Architecture (mode="spa") AppHandler serves a transformed index.html shell; client router owns navigation and fetches JSON from Litestar OpenAPI routes via @hey-api/openapi-ts and route(). HTML Shell Owner AppHandler (index.html) Injects window.__LITESTAR_CSRF__ Catch-all SPA fallback routes Navigation & Router Client Router React Router / Vue Router / TanStack route() & createRouteHelpers() Data Transport @hey-api/openapi-ts SDK JSON REST + csrfFetch() createTypedChannels() WS/SSE
Hybrid Inertia.js Architecture (mode="hybrid") Litestar controllers own routing and return component PageProps; Inertia.js swaps components via X-Inertia JSON and supports SSR via _InertiaSlots. HTML Shell & SSR AppHandler / Jinja + SSR data-page or <script id="app_page"> Optional Dev TCP / Prod Stdio SSR Server-Driven Routing @get(..., component="...") Python guards & DI own every route resolvePageComponent() on client Inertia.js Protocol PageProps[T] JSON defer() • merge() • lazy() • once() Generated page-props.ts types
Template + Server Islands Architecture (mode="template") Jinja2 templates render server HTML using vite() and vite_hmr(), progressively enhanced with HTMX swap="json", <litestar-stream>, and <litestar-island> fragments. Jinja2 / Server HTML {{ vite('src/main.ts') }} {{ vite_hmr() }} in development {{ vite_fragment('Chart.tsx') }} Server Fragments & Islands <litestar-island> ComponentResponse (HX-Push-Url) hydrateIslands() (React/Vue/Svelte) HTMX & Realtime Streams hx-swap="json" ls-for • ls-if • CSRF auto-header <litestar-stream> WebSocket/SSE
Meta-Framework Architecture (mode="framework" / "external") Astro, SvelteKit, or Nuxt owns its server/SSR pipeline while litestar-vite-plugin/{astro,sveltekit,nuxt} synchronizes ports, proxies /api, and runs typegen. Meta-Framework Server Astro / SvelteKit / Nuxt 4 litestar-vite-plugin/{astro,nuxt,...} Writes hotfile on server listen Proxy & Port Bridge .litestar.json + proxyMode Forwards /api/* to Litestar Routes HMR through Litestar port Shared TypeGen Output openapi.json + routes.json Typed load() / useFetch() calls Zero duplicate schema config

Comparison Matrix

Dimension spa hybrid (inertia) template (htmx) framework (ssr / ssg) external
HTML Shell index.html transformed by AppHandler index.html or Jinja transformed by InertiaPlugin Litestar Jinja2 templates Meta-framework server External CLI dev server
Router Client router (React Router, Vue Router, TanStack) Litestar route handlers + Inertia client router Litestar route handlers (Template / ComponentResponse) Meta-framework file router; Litestar serves /api/* Angular or external router
Asset Tags Automatic HTML script transform Automatic transform + data-page JSON script Explicit vite_hmr() and vite() in Jinja Meta-framework Vite pipeline External bundler
Dev Proxy proxy_mode="vite" (Vite assets + HMR) proxy_mode="vite" (Vite assets + HMR) proxy_mode="vite" (Vite assets + HMR) proxy_mode="proxy" (all non-API routes) proxy_mode="proxy" to external target
Data Contract OpenAPI SDK + route() Inertia PageProps + route() Jinja context + <litestar-island> OpenAPI SDK + route() OpenAPI SDK + route()

Mode Aliases

For readability across different frontend stacks, ViteConfig.mode accepts descriptive aliases that normalize to the canonical mode:

  • "spa" — Single-Page Application (index.html catch-all route served by Litestar).
  • "hybrid" or "inertia" — Inertia.js server-driven SPA.
  • "template" or "htmx" — Server-rendered Jinja2 templates, HTMX partials, and <litestar-island> fragments.
  • "framework", "ssr", or "ssg" — Meta-frameworks (Astro, Nuxt, SvelteKit) where Litestar proxies non-API routes in development.
  • "external" — Non-Vite build tools (such as Angular CLI ng serve) managed via RuntimeConfig(external_dev_server=ExternalDevServer(...)).

Configuring Each Mode in Python

docs/examples/learn/vite_config_modes.py
"""Comparison of the four ViteConfig operating modes in Litestar Vite."""

from __future__ import annotations

from pathlib import Path

from litestar_vite import InertiaConfig, PathConfig, RuntimeConfig, TypeGenConfig, ViteConfig


def build_spa_config() -> ViteConfig:
    """Configure Single-Page Application (SPA) mode with automatic OpenAPI SDK generation."""
    return ViteConfig(
        mode="spa",
        dev_mode=True,
        paths=PathConfig(resource_dir=Path("src"), bundle_dir=Path("dist")),
        runtime=RuntimeConfig(start_dev_server=False),
        types=TypeGenConfig(generate_sdk=True, generate_routes=True),
    )


def build_hybrid_config() -> ViteConfig:
    """Configure Inertia.js v2 Hybrid mode with root HTML shell transformation."""
    return ViteConfig(
        mode="hybrid",
        dev_mode=True,
        paths=PathConfig(resource_dir=Path("resources"), bundle_dir=Path("public/build")),
        inertia=InertiaConfig(root_template="index.html"),
        runtime=RuntimeConfig(start_dev_server=False),
    )


def build_template_config() -> ViteConfig:
    """Configure server-driven Jinja2 Template mode with Vite asset helpers."""
    return ViteConfig(
        mode="template",
        dev_mode=True,
        paths=PathConfig(resource_dir=Path("resources"), bundle_dir=Path("public/build")),
        runtime=RuntimeConfig(start_dev_server=False),
    )


def build_framework_config() -> ViteConfig:
    """Configure Meta-Framework mode for Astro, Nuxt, or SvelteKit."""
    return ViteConfig(
        mode="framework",
        dev_mode=True,
        runtime=RuntimeConfig(port=4321, start_dev_server=False),
        types=TypeGenConfig(generate_sdk=True, generate_routes=True),
    )


def main() -> None:
    """Validate all four operating mode configurations."""
    assert build_spa_config().mode == "spa"
    assert build_hybrid_config().mode == "hybrid"
    assert build_template_config().mode == "template"
    assert build_framework_config().mode == "framework"


__all__ = ("build_framework_config", "build_hybrid_config", "build_spa_config", "build_template_config", "main")

Mode Auto-Detection

When ViteConfig.mode is omitted (mode=None), ViteConfig.__post_init__ resolves the operating mode deterministically:

  1. If inertia is enabled (InertiaConfig or inertia=True) and root_template ends with .html -> mode="hybrid".
  2. If inertia is enabled and root_template is a Jinja template (.j2, .jinja, .jinja2) -> mode="hybrid" with Jinja rendering.
  3. If Litestar(template_config=...) is registered without Inertia -> mode="template".
  4. If an index.html exists in root_dir, resource_dir, or public_dir -> mode="spa".

Tip

Always set mode explicitly (mode="spa", mode="hybrid", mode="template", mode="framework", or mode="external") in production applications so your intent is self-documenting and validated at startup.