Skip to content
latest
Framework Packages / latest

Inertia

github.com/mbvlabs/andurel/pkg/inertia implements the Inertia v3 server protocol over pkg/anduin. It classifies requests, builds page objects, evaluates props and partial reloads, corrects redirects, writes Vite tags, and optionally calls an SSR renderer. The browser side is the official @inertiajs/* adapter; the scaffold uses React.

Renderer construction

Construct one renderer in internal/runtime and pass it to handlers. RendererConfig carries the protocol settings, and options add the application document and shared state.

 1renderer, err := inertia.NewRenderer(
 2    inertia.RendererConfig{
 3        ContainerID:         cfg.Inertia.ContainerID,
 4        BuildPathURL:        routes.ViteBuild.FullURL(hosts),
 5        EntryPoint:          cfg.Inertia.EntryPoint,
 6        ViteDevURL:          cfg.Inertia.ViteDevURL,
 7        SSRURL:              cfg.Inertia.SSRURL,
 8        SSRTimeout:          cfg.Inertia.SSRRequestTimeout,
 9        SSRMaxResponseBytes: cfg.Inertia.SSRMaxResponseBytes,
10    },
11    inertia.WithRoot(resources.Root),
12    inertia.WithAssetFS(assets.Files),
13    inertia.WithProjectName(cfg.App.ProjectName),
14    inertia.WithEnvironment(cfg.App.Environment),
15    inertia.WithShared(inertia.Props{"appVersion": cfg.Version}),
16    inertia.WithSharedProvider(sharedAuth),
17    inertia.WithSSRFailFast(cfg.Inertia.SSRFailFast),
18)

ContainerID is required and WithRoot must supply the document constructor. The other RendererConfig fields feed the Vite tags and the SSR client, so set them all; in production a missing asset filesystem or manifest entry also fails construction. Optional behavior includes WithVersion and WithVersionProvider (asset version), WithFlashProvider, WithReflash, WithProtocolDebug, and WithSSRRenderer.

The renderer has no lifecycle of its own. The application registers renderer.Middleware() on the router before the routes that render pages.

Page responses

A handler returns one builder, whether the request is a full browser visit or an Inertia visit:

1func (h PagesHandler) Home(ctx context.Context) error {
2    return h.renderer.Page(ctx, "Welcome", inertia.Props{}).Render()
3}

A normal request receives the root document with the page JSON and Vite tags. A request with X-Inertia: true receives only the JSON page object. Both add X-Inertia to Vary. The page object always carries component, props, url and version.

PageBuilder configures one response:

Method Effect
Status(int) HTTP status for the response
Errors(error) (*PageBuilder, bool) Sets the protected errors prop from a field-error error and reports whether it had any
ValidationErrors(map[string]string) Sets the protected errors prop directly
Flash(any) Adds the v3 top-level flash field
HistoryEncryption(bool), HistoryClear(), PreserveFragment() Browser history metadata
SSR() Renders this response through the configured SSR client
Render() Resolves props and writes the response

Root document and Vite

resources/root.templ is the only page-shaped template in the application. It renders the document shell and chooses between the SSR body and the client mount:

 1templ Root(data inertia.RootData) {
 2    <!DOCTYPE html>
 3    <html lang="en">
 4        <head>
 5            if data.SSR == nil {
 6                <title>{ data.ProjectName }</title>
 7            }
 8            @inertia.SSRHead(data.SSR)
 9            @templ.Raw(string(data.ViteHead))
10        </head>
11        <body>
12            if data.SSR != nil {
13                @inertia.SSRBody(data.SSR)
14            } else {
15                @inertia.PageScript(data.ContainerID, data.PageJSON)
16                @inertia.AppMount(data.ContainerID)
17            }
18            @templ.Raw(string(data.ViteBody))
19        </body>
20    </html>
21}

RootData exposes Page, PageJSON, ContainerID, ProjectName, Environment, ViteHead, ViteBody and SSR. PageScript validates and escapes the JSON so a prop cannot close the script element. Render a fallback <title> only when data.SSR == nil: SSR head markup is concatenated after the document head, so a static title would come first and win over the page's own.

In development, assets come from INERTIA_VITE_DEV_URL. In production they resolve through the embedded Vite manifest. BuildPathURL prefixes production tags and supplies the default asset version; a fixed WithVersion or a request-scoped WithVersionProvider overrides it.

Props

Use inertia.Props or inertia.FromStruct with a backend-owned payload struct. JSON tags define the browser names. Do not pass database entities or generated query rows directly.

Plain values resolve immediately. inertia.Resolver (func(context.Context) (any, error)) and any type implementing PropResolver resolve lazily. Policy constructors compose around a value or a resolver:

Constructor Behavior
Always Survives partial only/except filtering
Optional Omitted on full visits, resolved only when explicitly selected
Deferred(value, InGroup(...), Rescue()) Announced as a reload group; Rescue makes a failure recoverable
Merge, Prepend, DeepMerge Append, prepend or recursively merge into the retained client prop
MatchOn(value, paths...) Adds the matching paths used by a merge prop
Once(value, OnceKey(...), OnceFor(...), OnceExpiresAt(...), ForceFresh()) Retains a resolved prop on the client
Scroll(value, metadata), ScrollAt(value, wrapper, metadata) Infinite-scroll metadata plus merge behavior
 1props := inertia.Props{
 2    "account": inertia.Always(account),
 3    "filters": inertia.Optional(loadFilters),
 4    "stats":   inertia.Deferred(loadStats, inertia.InGroup("dashboard"), inertia.Rescue()),
 5    "products": inertia.Scroll(items, inertia.ScrollMetadata{
 6        PageName:    "products",
 7        CurrentPage: page,
 8        NextPage:    page + 1,
 9    }),
10    "countries": inertia.Once(loadCountries, inertia.OnceFor(24*time.Hour)),
11}

Nested dot paths participate in partial reloads. only is applied first, then except. Filtering only applies when X-Inertia-Partial-Component matches the component being rendered. Resolvers run only when the policy and the request require them.

Shared props, errors and flash

WithShared copies static props at construction. WithSharedProvider adds request-scoped values, such as the current visitor's sign-in state. Page props override shared props with the same name; errors is protected and cannot be supplied by either.

ValidationErrors and Errors populate props.errors, which is always an object. A named X-Inertia-Error-Bag nests the map under that bag. Flash is a top-level page field from PageBuilder.Flash, WithFlashProvider or inertia.ContextWithFlash.

After a write, Renderer.Redirect issues a normal redirect and the renderer middleware upgrades an unsafe 302 to 303 for POST, PUT, PATCH and DELETE visits. Location produces a 409 full browser visit for an Inertia request. FreshRedirect makes the client perform a fresh Inertia GET. An empty successful Inertia response redirects back to the referer.

SSR

SSR is per response: call PageBuilder.SSR(). A page without it is client-rendered even when an SSR runtime exists.

  • In development, the renderer posts to the Vite dev server's /__inertia_ssr endpoint derived from ViteDevURL, so the Vite dev server must be running; andurel run starts it.
  • Outside development, it posts to INERTIA_SSR_URL + /render.

cmd/ssr owns the Node process. It builds an inertia.SSRRuntime from INERTIA_SSR_RUNTIME, INERTIA_SSR_BUNDLE and INERTIA_SSR_LISTEN, waits for /health, and stops it on shutdown:

 1// cmd/ssr: cfg is the config.Inertia value from config.NewInertia.
 2ssr, err := inertia.NewSSRRuntime(inertia.SSRRuntimeConfig{
 3    Executable:     cfg.SSRRuntime,
 4    BundlePath:     cfg.SSRBundle,
 5    ListenURL:      cfg.SSRListen,
 6    StartupTimeout: cfg.SSRStartupTimeout,
 7    MinimumMajor:   cfg.SSRMinimumMajor,
 8    HealthTimeout:  cfg.SSRRequestTimeout,
 9    BundleFS:       assets.Files,
10})

pnpm build writes the client bundle and assets/dist/ssr/ssr.js. ValidateSSRListen requires an IP or localhost for the bind address; ValidateSSRClient accepts any HTTP(S) host for the client URL, including a service DNS name.

The package falls back to client rendering on an SSR failure unless WithSSRFailFast(true) is set. The scaffold wires that option from INERTIA_SSR_FAIL_FAST, whose config default is true; the shipped .env.example sets it to false for local development. SSRMaxResponseBytes bounds the response and returns inertia.ErrResponseTooLarge when exceeded.

Diagnostics and errors

INERTIA_PROTOCOL_DEBUG=true logs request classification and page metadata without logging prop or session values. Renderer errors are typed *inertia.Error with a kind (protocol, props, root, ssr), operation, component, method, URL, prop path and wrapped cause.

inertia.ErrorHandler converts handler errors into responses: field errors become a 422 page, mapped sentinels render the configured component, and 5xx details stay out of the body. The scaffold maps its domain errors in handlers.MapError:

1func MapError(renderer *inertia.Renderer) anduin.ErrorHandler {
2    return inertia.ErrorHandler(renderer,
3        inertia.MapError(domains.ErrNotFound, http.StatusNotFound, "Errors/NotFound"),
4        inertia.MapError(domains.ErrConflict, http.StatusConflict, "Errors/Conflict"),
5    )
6}

For unexpected behavior, inspect the X-Inertia-* headers, the exact component name, the selected nested paths, the asset version and the redirect status. For SSR, check the runtime version, bundle path, listen URL, timeouts and response limit separately from the page itself.