Auto-activate for litestar_vite.inertia, InertiaConfig, component=, @inertia, @inertiajs/*, createInertiaApp, useForm, usePage, Link, router, or pages/. Not for HTMX.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add litestar-org/litestar-skills --skill litestar-inertia --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Litestar Inertia?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/litestar-org-litestar-inertia-litestar-skills)More formats (shields.io, HTML) on the badges page.
---
name: litestar-inertia
description: "Auto-activate for litestar_vite.inertia, InertiaConfig, component=, @inertia, @inertiajs/*, createInertiaApp, useForm, usePage, Link, router, or pages/. Not for HTMX."
---
# Litestar + Inertia.js Integration
`litestar-inertia` is the four-library story:
| Layer | Library | Role |
| --- | --- | --- |
| Client SPA | [`@inertiajs/react`](https://inertiajs.com) / `@inertiajs/vue3` / `@inertiajs/svelte` | Page resolution, forms, navigation, shared data access; generated templates target Inertia v3 |
| Frontend build | [`vite`](https://vitejs.dev) | Bundling, HMR, dev server, production build |
| Python bridge | [`litestar-vite`](../litestar-vite/SKILL.md) | `VitePlugin` + `InertiaConfig`, asset manifest, type generation, page-props codec |
| Server framework | [`litestar`](../litestar/SKILL.md) | Routes, Controllers, Guards, DI, DTOs — returning Inertia responses |
Litestar routes produce page data, `ViteConfig.inertia` configures the response
layer, and the Vite-served client handles subsequent navigations. For Vite-only
configuration, use the sibling skill.
## When this skill activates
- Python files importing `litestar_vite.inertia`, `InertiaConfig`, or route handlers with `component=`
- `*.tsx` / `*.vue` / `*.svelte` files importing from `@inertiajs/*`
- `createInertiaApp({ resolve, setup })` in a frontend entrypoint
- A `resources/` or `resources/js/pages/` directory alongside a `src/py/` — classic litestar-vite + Inertia layout
- `inertia.config.ts` or an `InertiaConfig` invocation in `vite.config.ts`
- User asks about "building an SPA with a Python backend", "server-driven React/Vue", "form validation errors from Python", "shared auth data across pages"
## Code Style Rules
- **PEP 604 unions** in consumer Python modules; use
`from __future__ import annotations` only when the application benefits from it
- **TypeScript typed pages** — generate page-props types via `litestar-vite`'s TypeGen, never hand-roll
- **Forms via `useForm`** — use the adapter form helper for errors, submission
state, and navigation
- **CSRF via Litestar state** — configure Litestar `CSRFConfig` and wire
`csrfHeaders()` into global Inertia visit options; generated scaffolds already
do this, including with `cookie_httponly=True`
- **Shared data for auth + flash**, never page-specific. Static page props go in `InertiaConfig.extra_static_page_props`; session-backed props go in `extra_session_page_props`; request-time flashes use `share(request, ...)`.
- **camelCase on the wire** — define msgspec structs with
`class Example(msgspec.Struct, rename="camel")`; generated TypeScript consumes
the serialized names
- **Partial reloads** over full-page reloads when only a subset of props changes (`router.reload({ only: ['notifications'] })`)
- **Lazy props** for expensive-to-compute page data the user may not need on first paint
## Quick Reference
### Backend — Python route returning an Inertia page
```python
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):
"""Controller for user dashboard."""
path = "/dashboard"
guards = [requires_active_user]
@get("/", component="dashboard/Index")
async def index(self, dashboard_service) -> dict[str, Dashboard]:
"""Render dashboard page."""
return {"dashboard": await dashboard_service.get_for_current_user()}
```
→ See [references/litestar_integration.md](references/litestar_integration.md)
### Client — page component (React)
```tsx
// resources/js/pages/dashboard/Index.tsx
import { usePage, Head } from "@inertiajs/react";
import type { Dashboard } from "@/generated/api";
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](references/protocol.md)
### App wiring — VitePlugin owns the Inertia bridge
```python
from __future__ import annotations
from litestar import Litestar
from litestar.middleware.session.client_side import CookieBackendConfig
from litestar_vite import (
InertiaConfig,
InertiaSSRConfig,
PathConfig,
TypeGenConfig,
ViteConfig,
VitePlugin,
)
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},
precognition=True,
ssr=InertiaSSRConfig(
enabled=True,
url="http://127.0.0.1:13714/render",
command=["node", "resources/ssr.js"],
),
),
types=TypeGenConfig(output="resources/generated"),
)
)
app = Litestar(
route_handlers=[DashboardController],
plugins=[vite],
middleware=[session_backend.middleware],
)
```
→ See [references/litestar_integration.md](references/litestar_integration.md) for full wiring
### Forms — `useForm` with Litestar validation errors
```tsx
import { 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>
);
}
```
Inertia validation follows redirect-with-session semantics. Use `error(request,
field, message)` and return `InertiaBack(request)`, or install an exception
handler that performs that mapping. A raw `422` response does not populate the
next page's `errors` prop automatically.
### Precognition — Real-Time Form Validation
```python
from litestar import Request, post
from litestar_vite.inertia import InertiaRedirect, precognition
@post("/projects")
@precognition
async def create_project(data: ProjectCreateDTO, request: Request) -> InertiaRedirect:
"""Create a project for a non-Precognition submission."""
await project_service.create(data)
return InertiaRedirect(request, "/projects")
```
Set `InertiaConfig(precognition=True)`. A request with `Precognition: true`
that passes DTO validation receives `204 No Content` and skips the handler;
validation failures use the configured Precognition exception handler.
### Partial reloads & Prop Helpers
```python
from litestar import get
from litestar_vite.inertia import (
InertiaResponse,
always,
defer,
lazy,
merge,
once,
optional,
)
@get("/reports", component="reports/Index")
async def reports_page(reports_service) -> InertiaResponse:
"""Demonstrates all Inertia prop wrapper helpers."""
return InertiaResponse(
content={
"summary": await reports_service.summary(),
"auth": always("auth", {"canEdit": True}),
"settings": once("settings", reports_service.get_settings),
"comments": optional("comments", reports_service.get_comments),
"export": lazy("export", reports_service.export),
"stats": defer("stats", reports_service.fetch_stats, group="analytics"),
"items": merge("items", await reports_service.list_items(), strategy="append"),
}
)
```
<workflow>
## Workflow
### Step 1 — Wire the bridge
Register one `VitePlugin(config=ViteConfig(inertia=InertiaConfig(...)))`. Add
session middleware when using session-backed props or redirect errors. Do not
register a second Inertia plugin: `VitePlugin` reads `ViteConfig.inertia` and
configures the bridge.
### Step 2 — Define shared props
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.
### Step 3 — Set up the client entrypoint
`resources/js/app.tsx` (React) or equivalent: call `createInertiaApp()` with
`resolvePageComponent()`, render setup, and global visit options:
```ts
defaults: {
visitOptions: (_href, options) => ({
headers: csrfHeaders(options.headers ?? {}),
}),
}
```
Import `csrfHeaders` from `litestar-vite-plugin/helpers`.
### Step 4 — Build page components
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`.
### Step 5 — Generate types
`litestar assets generate-types` (from `litestar-vite`) reads your Python msgspec/DTO schemas and emits TypeScript types the page components consume directly.
### Step 6 — Validate
- `/` returns `text/html` (full initial render) on first visit
- Subsequent navigations return `application/json` with Inertia envelope (`X-Inertia: true`)
- DevTools Network tab shows `X-Inertia-*` response headers
- Invalid form input stores errors and redirects back; the next page response
exposes `errors`
- Stale asset-version `GET` requests return `409` plus
`X-Inertia-Location`; non-`GET` requests continue to the handler
- Partial responses omit `deferredProps`
</workflow>
<guardrails>
## Guardrails
- **Don't mix Inertia and plain JSON API routes in the same app surface** — pick one per domain. Mixing confuses auth, CSRF, and response shape expectations. If you need both, use separate route prefixes (`/api/*` for JSON, `/dashboard/*` for Inertia).
- **Do not assume `useForm` adds CSRF headers** — wire `csrfHeaders()` through
`createInertiaApp({ defaults: { visitOptions } })`; the shipped scaffolds do
this.
- **Shared props must be cheap** — session props are read on every page request. Cache user lookup; don't hit the DB for feature flags; use Redis for session state.
- **Version strings matter** — Inertia tracks an asset version; mismatched versions force a full page reload. Let `litestar-vite` generate the version hash; don't hand-roll.
- **No mixed-framework pages** — React + Vue in the same app breaks Inertia's resolver. Pick one adapter per project.
- **Deep-link routes need real URLs** — every Inertia page should have a Litestar route returning it. SPA-only client routes (React Router inside an Inertia page) exist but are an escape hatch.
- **Don't forget the root template** — `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.
</guardrails>
<validation>
## Validation Checkpoint
Before shipping an Inertia-integrated Litestar app:
- [ ] `ViteConfig.inertia` configured with `InertiaConfig(...)`
- [ ] One `VitePlugin` registered for Vite + Inertia
- [ ] Session middleware registered
- [ ] Static/session/request-time shared props have consistent shape across handlers
- [ ] Page components resolve via the resolver function (one place of truth for path→component mapping)
- [ ] TypeScript page-props types generated via `litestar assets generate-types`
- [ ] Forms use `useForm`
- [ ] Validation failures call `error()` and redirect back, or an exception
handler performs the same mapping
- [ ] `csrfHeaders()` is wired into global visit options
- [ ] `dev_mode` toggles correctly between dev (Vite HMR) and prod (manifest-resolved assets)
- [ ] Production build emits a configured or `.vite/` fallback manifest plus
hashed bundles
</validation>
<example>
## Example — Authenticated dashboard with forms + partial reload
```python
"""app/domain/projects/controllers.py"""
from __future__ import annotations
from litestar import Controller, Request, get, post
from litestar_vite.inertia import InertiaBack, error
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):
"""Projects management controller."""
path = "/projects"
guards = [requires_active_user]
@get("/", component="projects/Index")
async def index(self, projects_service: ProjectService, request: Request) -> dict[str, list[Project]]:
return {
"projects": await projects_service.list_for_user(request.user.id),
}
@post("/")
async def create(
self,
data: ProjectCreate,
projects_service: ProjectService,
request: Request,
) -> InertiaBack:
if await projects_service.exists(name=data.name, owner_id=request.user.id):
error(request, "name", "You already have a project with this name.")
return InertiaBack(request)
await projects_service.create(data.to_dict(), owner_id=request.user.id)
return InertiaBack(request)
```
```tsx
// resources/js/pages/projects/Index.tsx
import { useForm, usePage, router } from "@inertiajs/react";
import type { Project } from "@/generated/api";
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>
</>
);
}
```
</example>
## References Index
- **[Inertia Protocol & Client](references/protocol.md)** — Protocol v3, request/response shape, React/Vue/Svelte adapter setup, `useForm`, `usePage`, `router`, partial reloads, lazy props, SSR
- **[Litestar Backend Integration](references/litestar_integration.md)** — `InertiaConfig`, `component=` route handlers, shared props, redirect responses, validation errors, type generation, SSR server
## Cross-Skill References
- **[`../litestar-vite/SKILL.md`](../litestar-vite/SKILL.md)** — Vite plugin config, `VitePlugin`, asset manifest, TypeGen pipeline, HMR (the backbone Inertia sits on)
- **[`../litestar/SKILL.md`](../litestar/SKILL.md)** — Controllers, Guards, DI, DTO patterns (the request handling layer)
- **[`../advanced-alchemy/SKILL.md`](../advanced-alchemy/SKILL.md)** — Data services that produce page props
- **[`../msgspec/SKILL.md`](../msgspec/SKILL.md)** — Struct definitions that TypeGen consumes
## Official References
- Inertia.js v3 docs: <https://inertiajs.com/docs/v3>
- Tagged Litestar-Vite Inertia docs: <https://github.com/litestar-org/litestar-vite/tree/v0.31.0/docs/frameworks/inertia>
- Client-side setup: <https://inertiajs.com/docs/v3/installation/client-side-setup>
- Release notes: <https://github.com/inertiajs/inertia/releases>
- Tagged `litestar-vite` Inertia source: <https://github.com/litestar-org/litestar-vite/tree/v0.31.0/src/py/litestar_vite/inertia>
- Tagged Inertia tests: <https://github.com/litestar-org/litestar-vite/tree/v0.31.0/src/py/tests/unit/inertia>
## Shared Styleguide Baseline
- [General Principles](../litestar-styleguide/references/general.md)
- [Python](../litestar-styleguide/references/python.md)
- [TypeScript](../litestar-styleguide/references/typescript.md)
- [Litestar](../litestar-styleguide/references/litestar.md)
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.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!