Skip to content

Quick Start: Single-Page Application

In SPA mode (mode="spa"), Litestar owns your JSON API routes and serves a transformed index.html shell for all unmatched non-API GET routes via AppHandler. Your frontend router (react-router, vue-router, or TanStack Router) handles client-side navigation while TypeGenConfig generates a fully typed TypeScript SDK from Litestar's OpenAPI schema.


Install Dependencies

uv add litestar-vite
pip install litestar-vite
pdm add litestar-vite
poetry add litestar-vite

And in your frontend workspace:

npm install -D litestar-vite-plugin @hey-api/openapi-ts
pnpm add -D litestar-vite-plugin @hey-api/openapi-ts
bun add -d litestar-vite-plugin @hey-api/openapi-ts

Configure Litestar

Register VitePlugin with mode="spa" and enable TypeGenConfig so Litestar exports openapi.json and routes.json on startup:

docs/examples/getting_started/spa_app.py
"""Single-Page Application (SPA) quick-start example with Litestar and Vite."""

from __future__ import annotations

from pathlib import Path

from litestar import Litestar, get
from msgspec import Struct

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


class SummaryStats(Struct, frozen=True, gc=False):
    """Dashboard summary payload returned by the JSON API."""

    active_users: int
    requests_per_second: float
    region: str


@get("/api/summary", sync_to_thread=False)
def get_summary() -> SummaryStats:
    """Return typed summary metrics for the SPA frontend."""
    return SummaryStats(active_users=128, requests_per_second=412.5, region="us-east-1")


def create_app() -> Litestar:
    """Create a Litestar application configured for SPA mode."""
    vite_config = ViteConfig(
        mode="spa",
        dev_mode=True,
        paths=PathConfig(root=Path(), resource_dir=Path("src"), bundle_dir=Path("public/build")),
        runtime=RuntimeConfig(start_dev_server=False),
        types=TypeGenConfig(generate_sdk=True, generate_routes=True),
    )
    return Litestar(route_handlers=[get_summary], plugins=[VitePlugin(config=vite_config)])


def main() -> None:
    """Verify the SPA example application constructs cleanly."""
    app = create_app()
    plugin = app.plugins.get(VitePlugin)
    assert plugin.config.mode == "spa"
    assert SummaryStats(active_users=128, requests_per_second=412.5, region="us-east-1").active_users == 128


__all__ = ("SummaryStats", "create_app", "get_summary", "main")

Configure Vite

Point litestar-vite-plugin at your application entrypoint (src/main.tsx or src/main.ts). All path, port, and typegen settings are automatically synchronized from .litestar.json:

vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import litestar from "litestar-vite-plugin";

export default defineConfig({
  plugins: [
    react(),
    litestar({
      input: ["src/main.tsx"],
    }),
  ],
});

Create the HTML Shell and Run

Place index.html in your project root (or resource_dir). In mode="spa", AppHandler automatically injects @vite/client (and the React Fast Refresh preamble when is_react=True) in development, and swaps in hashed manifest.json bundle tags in production:

index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Litestar SPA</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

Start the unified development server:

litestar run --reload

Open http://127.0.0.1:8000 in your browser:

  • GET /api/summary returns the JSON SummaryStats response from Litestar.
  • GET / (and any client route like /settings) serves the transformed index.html shell.
  • /src/main.tsx and /vite-hmr stream through ViteProxyMiddleware over port 8000.