From litestar
Litestar + Inertia.js integration: server-driven React/Vue/Svelte SPAs with Python backend. Covers route handlers returning Inertia pages, useForm-based forms, partial reloads, lazy props, shared auth/flash data, and typed page props via litestar-vite.
How this skill is triggered — by the user, by Claude, or both
Slash command
/litestar:litestar-inertiaThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
`litestar-inertia` is the four-library story:
litestar-inertia is the four-library story:
| Layer | Library | Role |
|---|---|---|
| Client SPA | @inertiajs/react / @inertiajs/vue3 / @inertiajs/svelte | Page resolution, forms, navigation, shared data access |
| Frontend build | vite | Bundling, HMR, dev server, production build |
| Python bridge | litestar-vite | VitePlugin + InertiaConfig, asset manifest, type generation, page-props codec |
| Server framework | litestar | Routes, Controllers, Guards, DI, DTOs — returning Inertia responses |
You can't skip any of these. Using Inertia with Litestar requires all four, and the skills form a chain: Litestar routes produce page data → ViteConfig.inertia configures the Inertia response layer → Vite-served client bundle mounts the page component → Inertia client takes over for subsequent navigations.
This skill covers that integration end-to-end. For anything that's purely about one layer (e.g., Vite config internals) see the corresponding sibling skill.
litestar_vite.inertia, InertiaConfig, or route handlers with component=*.tsx / *.vue / *.svelte files importing from @inertiajs/*createInertiaApp({ resolve, setup }) in a frontend entrypointresources/ or resources/js/pages/ directory alongside a src/py/ — classic litestar-vite + Inertia layoutinertia.config.ts or an InertiaConfig invocation in vite.config.tsfrom __future__ import annotations in consumer Python modules — standard Litestar rules applylitestar-vite's TypeGen, never hand-rolluseForm — never a plain <form onSubmit>. useForm handles CSRF, errors, submission state, and navigation in one callInertiaConfig.extra_static_page_props; session-backed props go in extra_session_page_props; request-time flashes use share(request, ...).Meta(rename="camel"), JS consumes camelCase directlyrouter.reload({ only: ['notifications'] }))from __future__ import annotations
from litestar import Controller, get
from app.domain.accounts.guards import requires_active_user
from app.domain.dashboard.schemas import Dashboard
class DashboardController(Controller):
path = "/dashboard"
guards = [requires_active_user]
@get("/", component="dashboard/Index")
async def index(self, dashboard_service) -> Dashboard:
return await dashboard_service.get_for_current_user()
→ See references/litestar_integration.md
// resources/js/pages/dashboard/Index.tsx
import { usePage, Head } from "@inertiajs/react";
import type { Dashboard } from "@/types/generated"; // TypeGen output
export default function DashboardIndex() {
const { dashboard } = usePage<{ dashboard: Dashboard }>().props;
return (
<>
<Head title="Dashboard" />
<h1>Welcome, {dashboard.user.name}</h1>
<p>Your workspace has {dashboard.workspaceCount} projects.</p>
</>
);
}
→ See references/protocol.md
from __future__ import annotations
from litestar import Litestar
from litestar.middleware.session.client_side import CookieBackendConfig
from litestar_granian import GranianPlugin
from litestar_vite import PathConfig, TypeGenConfig, ViteConfig, VitePlugin
from litestar_vite.inertia import InertiaConfig
from app.domain.accounts.schemas import CurrentUser
from app.lib.settings import get_settings
settings = get_settings()
session_backend = CookieBackendConfig(secret=settings.secret_key.encode("utf-8"))
vite = VitePlugin(
config=ViteConfig(
mode="hybrid",
dev_mode=settings.debug,
paths=PathConfig(
root=settings.base_dir,
resource_dir="resources",
bundle_dir="public",
),
inertia=InertiaConfig(
root_template="index.html",
extra_static_page_props={"appName": settings.app_name},
extra_session_page_props={"currentUser": CurrentUser},
),
types=TypeGenConfig(output="resources/generated"),
)
)
app = Litestar(
route_handlers=[DashboardController, ...],
plugins=[GranianPlugin(), vite],
middleware=[session_backend.middleware],
)
→ See references/litestar_integration.md for full wiring
useForm with Litestar validation errorsimport { useForm } from "@inertiajs/react";
export default function CreateProject() {
const { data, setData, post, processing, errors } = useForm({
name: "",
description: "",
});
return (
<form onSubmit={(e) => { e.preventDefault(); post("/projects"); }}>
<input value={data.name} onChange={(e) => setData("name", e.target.value)} />
{errors.name && <div className="error">{errors.name}</div>}
<textarea value={data.description} onChange={(e) => setData("description", e.target.value)} />
{errors.description && <div className="error">{errors.description}</div>}
<button type="submit" disabled={processing}>Create</button>
</form>
);
}
On the Python side, raising a ValidationException with a dict of field errors auto-maps into errors on the client — no manual serialization.
import { router } from "@inertiajs/react";
// After a background task finishes, reload only notifications:
router.reload({ only: ["notifications"] });
from litestar import get
from litestar_vite.inertia import lazy
@get("/reports", component="reports/Index")
async def reports_page(self, reports_service) -> dict:
return {
"summary": await reports_service.summary(), # eager
"fullExport": lazy(lambda: reports_service.export()), # deferred
}
Client fetches fullExport only on router.reload({ only: ["fullExport"] }).
Register GranianPlugin and one VitePlugin(config=ViteConfig(inertia=InertiaConfig(...))). Add session middleware. Do not register a second Inertia plugin in normal app scaffolds; VitePlugin reads ViteConfig.inertia and configures the Inertia bridge.
Put static values in InertiaConfig.extra_static_page_props. Put session-backed values in extra_session_page_props so the integration pulls them from request.session. For request-time flash/auth additions, call share(request, key, value) before returning an Inertia response. These are available on every page via usePage().props without threading them through each handler.
resources/js/app.tsx (React) or equivalent: createInertiaApp({ resolve: (name) => resolvePageComponent(name, ...), setup: ({ el, App, props }) => createRoot(el).render(<App {...props} />) }).
One .tsx / .vue / .svelte file per route, keyed by name. @get(..., component="path/Name") on the Python handler maps to resources/js/pages/path/Name.tsx.
litestar assets generate-types (from litestar-vite) reads your Python msgspec/DTO schemas and emits TypeScript types the page components consume directly.
/ returns text/html (full initial render) on first visitapplication/json with Inertia envelope (X-Inertia: true)X-Inertia-* response headersuseForm().post() with invalid data returns 422 + errors populated/api/* for JSON, /dashboard/* for Inertia).useForm handles the token transparently.litestar-vite generate the version hash; don't hand-roll.InertiaConfig.root_template points at the template that mounts the SPA. Default is index.html; Jinja-backed Inertia apps set a Litestar TemplateConfig as well.Before shipping an Inertia-integrated Litestar app:
ViteConfig.inertia configured with InertiaConfig(...)VitePlugin registered for Vite + Inertialitestar assets generate-typesuseForm, not bare <form>errors that the client readsuseForm picks it up automaticallydev_mode toggles correctly between dev (Vite HMR) and prod (manifest-resolved assets)litestar assets build) emits public/manifest.json + hashed bundles# app/domain/projects/controllers.py
from __future__ import annotations
from litestar import Controller, get, post
from litestar.exceptions import ValidationException
from litestar_vite.inertia import back
from app.domain.accounts.guards import requires_active_user
from app.domain.projects.schemas import Project, ProjectCreate
from app.domain.projects.services import ProjectService
class ProjectsController(Controller):
path = "/projects"
guards = [requires_active_user]
@get("/", component="projects/Index")
async def index(self, projects_service: ProjectService, request) -> dict:
return {
"projects": await projects_service.list_for_user(request.user.id),
}
@post("/")
async def create(
self, data: ProjectCreate, projects_service: ProjectService, request,
) -> None:
# Validation
if await projects_service.exists(name=data.name, owner_id=request.user.id):
raise ValidationException(extra={"name": "You already have a project with this name."})
await projects_service.create(data.to_dict(), owner_id=request.user.id)
# `back()` redirects Inertia back to the previous page with flash data intact
return back()
// resources/js/pages/projects/Index.tsx
import { useForm, usePage, router } from "@inertiajs/react";
import type { Project } from "@/types/generated";
export default function ProjectsIndex() {
const { projects, flash } = usePage<{ projects: Project[]; flash: { success?: string } }>().props;
const { data, setData, post, processing, errors, reset } = useForm({ name: "", description: "" });
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
post("/projects", { onSuccess: () => reset() });
};
return (
<>
{flash.success && <div className="flash">{flash.success}</div>}
<form onSubmit={onSubmit}>
<input value={data.name} onChange={(e) => setData("name", e.target.value)} placeholder="Project name" />
{errors.name && <div className="error">{errors.name}</div>}
<textarea value={data.description} onChange={(e) => setData("description", e.target.value)} />
<button disabled={processing}>Create</button>
</form>
<button onClick={() => router.reload({ only: ["projects"] })}>Refresh</button>
<ul>
{projects.map((p) => <li key={p.id}>{p.name}</li>)}
</ul>
</>
);
}
useForm, usePage, router, partial reloads, lazy props, SSRInertiaConfig, component= route handlers, shared props, back(), validation errors, type generation, SSR server../litestar-vite/SKILL.md — Vite plugin config, VitePlugin, asset manifest, TypeGen pipeline, HMR (the backbone Inertia sits on)../litestar/SKILL.md — Controllers, Guards, DI, DTO patterns (the request handling layer)../advanced-alchemy/SKILL.md — Data services that produce page props../msgspec/SKILL.md — Struct definitions that TypeGen consumesThe canonical Litestar + Inertia stack lives at litestar-fullstack-inertia. When in doubt about wiring or file layout, mirror it.
litestar-vite Inertia docs: https://litestar-org.github.io/litestar-vite/inertia/litestar-vite Inertia API: https://litestar-org.github.io/litestar-vite/reference/inertia/Keep this skill focused on the Litestar ↔ Vite ↔ Inertia integration surface. Framework-agnostic React/Vue/Svelte patterns belong in the respective framework skills (if we ever port them) or inertiajs.com docs.
npx claudepluginhub litestar-org/litestar-skills --plugin litestarSearches MemPalace before answering questions about past work, people, projects, or prior decisions. Returns verbatim stored content instead of guessing from model memory.
Guides Payload CMS config (payload.config.ts), collections, fields, hooks, access control, APIs. Debugs validation errors, security, relationships, queries, transactions, hook behavior.
Implements vector databases with Pinecone, Weaviate, Qdrant, Milvus, pgvector for semantic search, RAG, recommendations, and similarity systems. Optimizes embeddings, indexing, and hybrid search.