Skip to content

Pages & Responses

In mode="hybrid", any Litestar route handler can render an Inertia page component by setting component="..." (or page="...") in the route decorator and returning a dictionary or typed model.


Declaring Page Handlers

You can return a plain dict[str, Any] or msgspec.Struct when component="..." is declared on the route decorator, or return an explicit InertiaResponse when you need per-response flags such as encrypt_history=True, clear_history=True, template_name, or prop_filter:

docs/examples/inertia/pages_and_responses.py
"""Inertia.js v2 page components, InertiaResponse, and redirects."""

from __future__ import annotations

from typing import Any

from litestar import Litestar, Request, get, post
from msgspec import Struct

from litestar_vite import InertiaConfig, RuntimeConfig, ViteConfig, VitePlugin
from litestar_vite.inertia import InertiaExternalRedirect, InertiaRedirect, InertiaResponse


class InvoiceSummary(Struct, frozen=True, gc=False):
    """Typed invoice item serialized into Inertia PageProps."""

    id: int
    customer: str
    total_cents: int


@get("/invoices", component="Invoices/Index", sync_to_thread=False)
def list_invoices() -> dict[str, Any]:
    """Return Inertia page props via the ``component=`` route option."""
    return {
        "invoices": [
            InvoiceSummary(id=101, customer="Acme Corp", total_cents=12500),
            InvoiceSummary(id=102, customer="Globex", total_cents=8400),
        ]
    }


@get("/invoices/featured", component="Invoices/Show", sync_to_thread=False)
def show_featured_invoice() -> InertiaResponse[InvoiceSummary]:
    """Return an explicit ``InertiaResponse`` with history encryption enabled."""
    return InertiaResponse(InvoiceSummary(id=101, customer="Acme Corp", total_cents=12500), encrypt_history=True)


@post("/invoices/checkout", sync_to_thread=False)
def start_external_checkout(request: Request[Any, Any, Any]) -> InertiaExternalRedirect:
    """Trigger an external ``409 Conflict`` + ``X-Inertia-Location`` redirect."""
    return InertiaExternalRedirect(request, redirect_to="https://billing.example.com/checkout")


def create_app() -> Litestar:
    """Create a Litestar app with Inertia v2 page and redirect handlers."""
    vite_config = ViteConfig(
        mode="hybrid",
        dev_mode=True,
        inertia=InertiaConfig(root_template="index.html"),
        runtime=RuntimeConfig(start_dev_server=False),
    )
    return Litestar(
        route_handlers=[list_invoices, show_featured_invoice, start_external_checkout],
        plugins=[VitePlugin(config=vite_config)],
    )


def main() -> None:
    """Verify Inertia page responses and redirect primitives."""
    app = create_app()
    assert app is not None
    response = InertiaResponse(InvoiceSummary(id=101, customer="Acme Corp", total_cents=12500), encrypt_history=True)
    assert response.encrypt_history is True
    assert InertiaRedirect is not None


__all__ = ("InvoiceSummary", "create_app", "list_invoices", "main", "show_featured_invoice", "start_external_checkout")

Nested Page Components

Organize page components into subdirectories (resources/pages/Users/Index.tsx, resources/pages/Settings/Profile.tsx) and reference them with forward-slash paths (component="Users/Index"). resolvePageComponent maps the component string directly to ./pages/${name}.tsx (or .vue / .svelte).


Direct Model Returns

When a route handler returns a msgspec.Struct, dataclass, or Pydantic BaseModel directly instead of a dict, InertiaResponse serializes its top-level fields as the page's props — respecting msgspec.Struct(rename="camel") and msgspec.field(name=...) aliases automatically:

from litestar import get
from msgspec import Struct


class UserProfileProps(Struct, rename="camel"):
    user_id: int
    display_name: str
    is_verified: bool


@get("/profile", component="Profile/Show", sync_to_thread=False)
def show_profile() -> UserProfileProps:
    """Serializes as { userId: 42, displayName: 'Ada', isVerified: true }."""
    return UserProfileProps(user_id=42, display_name="Ada", is_verified=True)

Consuming Page Props on the Frontend

resources/pages/Profile/Show.tsx
import { Head } from "@inertiajs/react";

interface ProfileShowProps {
  userId: number;
  displayName: string;
  isVerified: boolean;
}

export default function ProfileShow({ displayName, isVerified }: ProfileShowProps) {
  return (
    <>
      <Head title={`${displayName} — Profile`} />
      <h1>{displayName}</h1>
      {isVerified && <span>Verified</span>}
    </>
  );
}
resources/pages/Profile/Show.vue
<script setup lang="ts">
import { Head } from "@inertiajs/vue3";

defineProps<{
  userId: number;
  displayName: string;
  isVerified: boolean;
}>();
</script>

<template>
  <Head :title="`${displayName} — Profile`" />
  <h1>{{ displayName }}</h1>
  <span v-if="isVerified">Verified</span>
</template>
resources/pages/Profile/Show.svelte
<script lang="ts">
  interface Props {
    userId: number;
    displayName: string;
    isVerified: boolean;
  }

  let { displayName, isVerified }: Props = $props();
</script>

<svelte:head>
  <title>{displayName} — Profile</title>
</svelte:head>

<h1>{displayName}</h1>
{#if isVerified}
  <span>Verified</span>
{/if}

Automatic Pagination Container Unwrapping

When a route handler returns a Litestar OffsetPagination[T], ClassicPagination[T], or advanced_alchemy pagination container, InertiaResponse automatically extracts the items into the prop key specified by key="..." (default "items") alongside pagination metadata (total, limit, offset, or current_page, total_pages):

from litestar import get
from litestar.pagination import OffsetPagination
from msgspec import Struct


class PostSummary(Struct):
    id: int
    title: str


@get("/posts", component="Posts/Index", key="posts", sync_to_thread=False)
def list_posts(limit: int = 10, offset: int = 0) -> OffsetPagination[PostSummary]:
    """Unwraps items into props['posts'] with pagination metadata."""
    items = [PostSummary(id=1, title="Hello Litestar")]
    return OffsetPagination(items=items, limit=limit, offset=offset, total=1)

Server-Side Prop Filtering (only and except_)

Use only(*keys) or except_(*keys) with InertiaResponse(..., prop_filter=...) to restrict which keys from a shared service dictionary or model are serialized into props:

from typing import Any
from litestar import Request, get
from litestar_vite.inertia import InertiaResponse, except_


@get("/users/{user_id:int}", component="Users/Show", sync_to_thread=False)
def show_user(request: Request[Any, Any, Any], user_id: int) -> InertiaResponse[dict[str, Any]]:
    """Exclude internal audit fields on the server before serialization."""
    record = {"id": user_id, "name": "Alice", "password_hash": "...", "internal_notes": "..."}
    return InertiaResponse(
        record,
        prop_filter=except_("password_hash", "internal_notes"),
    )