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:
"""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¶
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>}
</>
);
}
<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>
<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"),
)