Astro

Astro integration with Litestar Vite for content-focused sites with optional islands of interactivity.

Quick Start

litestar assets init --template astro

This creates an Astro project with TypeScript support and multi-framework component support.

Project Structure

Astro applications use the Astro integration:

my-app/
├── app.py              # Litestar backend
├── package.json
├── astro.config.mjs    # Astro configuration with Litestar integration
├── tsconfig.json
└── src/
    ├── pages/
    │   └── index.astro     # Pages (file-based routing)
    ├── components/
    │   └── Card.astro      # Astro components
    └── generated/          # Generated types from OpenAPI

Backend Setup

examples/astro/app.py
"""Astro example - shared "Library" backend + Astro static site frontend.

All examples in this repository expose the same backend:
- `/api/summary` - overview + featured book
- `/api/books` - list of books
- `/api/books/{book_id}` - single book

Dev mode (default):
    litestar --app-dir examples/astro run

Production mode (serves static build):
    VITE_DEV_MODE=false litestar --app-dir examples/astro run
"""

import os
from pathlib import Path

from litestar import Controller, Litestar, get
from litestar.exceptions import NotFoundException
from msgspec import Struct

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

here = Path(__file__).parent
DEV_MODE = os.getenv("VITE_DEV_MODE", "true").lower() in {"true", "1", "yes"}


class Book(Struct):
    id: int
    title: str
    author: str
    year: int
    tags: list[str]


class Summary(Struct):
    app: str
    headline: str
    total_books: int
    featured: Book


BOOKS: list[Book] = [
    Book(id=1, title="Async Python", author="C. Developer", year=2024, tags=["python", "async"]),
    Book(id=2, title="Type-Safe Web", author="J. Dev", year=2025, tags=["typescript", "api"]),
    Book(id=3, title="Frontend Patterns", author="A. Designer", year=2023, tags=["frontend", "ux"]),
]


def _get_book(book_id: int) -> Book:
    for book in BOOKS:
        if book.id == book_id:
            return book
    raise NotFoundException(detail=f"Book {book_id} not found")


def _get_summary() -> Summary:
    """Build summary data."""
    return Summary(
        app="litestar-vite library", headline="One backend, many frontends", total_books=len(BOOKS), featured=BOOKS[0]
    )


class LibraryController(Controller):
    """Library API controller."""

    @get("/api/summary")
    async def summary(self) -> Summary:
        """Overview endpoint used across all examples."""
        return _get_summary()

    @get("/api/books")
    async def books(self) -> list[Book]:
        """Return all books."""
        return BOOKS

    @get("/api/books/{book_id:int}")
    async def book_detail(self, book_id: int) -> Book:
        """Return a single book by id."""
        return _get_book(book_id)


vite = VitePlugin(
    config=ViteConfig(
        mode="ssg",  # Static Site Generation: proxy in dev, serve static in prod
        dev_mode=DEV_MODE,
        paths=PathConfig(
            root=here,
            bundle_dir=Path("dist"),  # Astro outputs to dist/ by default
        ),
        types=TypeGenConfig(generate_zod=True),
        # Fixed port for E2E tests - can be removed for local dev or customized for production
        runtime=RuntimeConfig(port=5051),
    )
)

app = Litestar(route_handlers=[LibraryController], plugins=[vite], debug=True)

Key points:

  • mode="framework" enables meta-framework integration mode (aliases: mode="ssr" / mode="ssg")

  • ExternalDevServer delegates dev server to Astro

  • TypeGenConfig enables type generation for Astro

Astro Configuration

astro.config.mjs
import tailwindcss from "@tailwindcss/vite"
import { defineConfig } from "astro/config"
import litestar from "litestar-vite-plugin/astro"

// Litestar manages the dev server port via VITE_PORT and runtime config.
// The Astro integration reads the port automatically - no hardcoding needed.
// LITESTAR_PORT is the backend API server port (default 8000).
const LITESTAR_PORT = process.env.LITESTAR_PORT ?? "8000"

export default defineConfig({
  integrations: [
    litestar({
      // API proxy points to the Litestar backend
      apiProxy: `http://localhost:${LITESTAR_PORT}`,
      apiPrefix: "/api",
      // Match Python TypeGenConfig paths
      types: {
        output: "src/generated",
        openapiPath: "src/generated/openapi.json",
        routesPath: "src/generated/routes.json",
      },
    }),
  ],
  vite: {
    plugins: [tailwindcss()],
  },
})

The Litestar integration provides:

  • API proxy configuration (apiProxy)

  • Type generation integration

  • Automatic port coordination with Litestar backend

Configuration options:

  • apiProxy - URL of Litestar backend (default: http://localhost:8000)

  • apiPrefix - API route prefix to proxy (default: /api)

  • types - Type generation configuration

API Integration

Server-Side Data Fetching

Astro pages can fetch data at build time or on-demand (SSR):

---
// src/pages/users/[id].astro
import type { User } from '../../generated/types.gen'
import { route } from '../../generated/routes'

const { id } = Astro.params
const response = await fetch(route('users:show', { id }))
const user: User = await response.json()
---

<html>
  <head>
    <title>{user.name}</title>
  </head>
  <body>
    <h1>{user.name}</h1>
    <p>{user.email}</p>
  </body>
</html>

Static Site Generation (SSG)

Use getStaticPaths for static generation:

---
import type { User } from '../../generated/types.gen'

export async function getStaticPaths() {
  const response = await fetch('http://localhost:8000/api/users')
  const users: User[] = await response.json()

  return users.map(user => ({
    params: { id: user.id },
    props: { user },
  }))
}

const { user } = Astro.props
---

<h1>{user.name}</h1>

Client-Side Interactivity

Add interactive islands with client:* directives:

---
// src/pages/index.astro
import Counter from '../components/Counter'
---

<html>
  <body>
    <h1>Welcome</h1>
    <!-- Only hydrates on client when visible -->
    <Counter client:visible />
  </body>
</html>
// src/components/Counter.tsx (React, Vue, Svelte, etc.)
import { useState } from 'react'
import { route } from '../generated/routes'

export default function Counter() {
  const [count, setCount] = useState(0)
  const [summary, setSummary] = useState(null)

  async function loadSummary() {
    const res = await fetch(route('summary'))
    setSummary(await res.json())
  }

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <button onClick={loadSummary}>Load Summary</button>
      {summary && <p>{summary.headline}</p>}
    </div>
  )
}

Running

# Recommended: Litestar manages both servers
litestar run --reload

# Alternative: Run separately
litestar assets serve --production  # Astro server
litestar run --reload               # Backend API (in another terminal)

Type Generation

With types=TypeGenConfig() enabled in Python:

litestar assets generate-types

This generates TypeScript types in src/generated/ (path configured in astro.config.mjs).

Deployment

For production:

  1. Build Astro:

    litestar assets build
    
  2. Serve the built site:

    • Framework SSR: litestar assets serve --production runs the Astro server

    • Static mode: Serve dist/ via Litestar’s static files handler

Rendering Modes

Astro supports multiple rendering modes:

Mode

Use Case

Output

Static (SSG)

Blogs, marketing sites

Pre-built HTML files

Server (SSR)

Dynamic content, auth

On-demand rendering

Hybrid

Mix of static + dynamic

Some pre-built, some on-demand

Configure in astro.config.mjs:

export default defineConfig({
  output: 'server',  // 'static', 'server', or 'hybrid'
  integrations: [litestar({ /* ... */ })],
})

See Also