Inertia.js

Build modern SPAs with Vue, React, or Svelte without building a separate API. Inertia lets your Go backend render pages and pass props; the frontend handles routing and UI. Powered by github.com/petaki/inertia-go.

Installation

nimbus plugin install inertia

Or scaffold a full Inertia app with a frontend kit:

nimbus create my-app --kit=react
nimbus create my-app --kit=vue
nimbus create my-app --kit=svelte

Register the Plugin

import "github.com/CodeSyncr/nimbus/plugins/inertia"

app.Use(inertia.New(inertia.Config{
    URL:          "http://localhost:3333",
    RootTemplate: "resources/views/inertia_layout.nimbus",
    Version:      "1",
}))

Config Options

FieldDescription
URLApplication URL (used in root template for redirects)
RootTemplatePath to root HTML template
VersionAsset version string for cache busting
SSRURLNode.js SSR server URL (optional, e.g. http://localhost:13714)
TemplateFSCustom fs.FS for the root template (optional)

Rendering Pages

Use inertia.Render() in handlers. When the browser makes a standard request, it returns full HTML. When the Inertia client sends an XHR (with X-Inertia header), it returns JSON page data:

func ShowDashboard(c *http.Context) error {
    return inertia.Render(c, "Dashboard/Index", map[string]any{
        "stats": loadDashboardStats(),
        "user":  auth.UserFromContext(c.Ctx()),
    })
}

The component name (Dashboard/Index) maps to inertia/pages/Dashboard/Index.tsx (or .vue / .svelte).

Shared Data

Global Sharing

Share data available to every Inertia response. Only for static data that never changes per-request:

inertia.Share("appName", "My App")
inertia.Share("version", "2.1.0")

Request-Scoped Sharing (ShareProp)

ShareProp() shares data for the current request only โ€” safe for concurrent requests. Use this for user-specific data:

// In auth middleware
func AuthMiddleware(next router.HandlerFunc) router.HandlerFunc {
    return func(c *http.Context) error {
        user := auth.UserFromContext(c.Ctx())

        inertia.ShareProp(c, "auth", map[string]any{
            "user": user,
        })

        // Share flash messages
        if c.Session != nil {
            inertia.ShareProp(c, "flash", map[string]any{
                "success": c.Session.GetFlash("success"),
                "error":   c.Session.GetFlash("error"),
            })
        }

        return next(c)
    }
}

Shared props merge into every inertia.Render() call automatically. Props passed directly to Render() take precedence over shared props with the same key.

Root Template

The root template wraps your SPA. It must include a data-page div and load your frontend entry:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    {{ if .viteDev }}
    <script type="module" src="http://localhost:5173/@vite/client"></script>
    <script type="module" src="http://localhost:5173/src/main.js"></script>
    {{ else }}
    <link rel="stylesheet" href="/build/assets/app.css">
    {{ end }}
</head>
<body>
    <div id="app" data-page="{{ marshal .page }}"></div>
    {{ if not .viteDev }}
    <script src="/build/assets/app.js"></script>
    {{ end }}
</body>
</html>

When VITE_DEV=1 (set automatically by nimbus serve for Inertia apps), the template loads from the Vite dev server for HMR. In production, it loads built assets from /build/. If no template file exists, an embedded default is used.

Frontend Setup

React + Vite

// inertia/app.tsx
import { createInertiaApp } from '@inertiajs/react'
import { createRoot } from 'react-dom/client'

createInertiaApp({
    resolve: name => {
        const pages = import.meta.glob('./pages/**/*.tsx', { eager: true })
        return pages[`./pages/${name}.tsx`]
    },
    setup({ el, App, props }) {
        createRoot(el).render(<App {...props} />)
    },
})

Vue 3 + Vite

// inertia/app.js
import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'

createInertiaApp({
    resolve: name => {
        const pages = import.meta.glob('./pages/**/*.vue', { eager: true })
        return pages[`./pages/${name}.vue`]
    },
    setup({ el, App, props, plugin }) {
        createApp({ render: () => h(App, props) })
            .use(plugin)
            .mount(el)
    },
})

Svelte + Vite

// inertia/app.js
import { createInertiaApp } from '@inertiajs/svelte'

createInertiaApp({
    resolve: name => {
        const pages = import.meta.glob('./pages/**/*.svelte', { eager: true })
        return pages[`./pages/${name}.svelte`]
    },
    setup({ el, App, props }) {
        new App({ target: el, props })
    },
})

Server-Side Rendering (SSR)

Enable SSR by setting the SSRURL config to your Node.js SSR server:

app.Use(inertia.New(inertia.Config{
    URL:          "http://localhost:3333",
    RootTemplate: "resources/views/inertia_layout.nimbus",
    Version:      "1",
    SSRURL:       "http://localhost:13714",
}))

Set up a Node.js SSR server using @inertiajs/vue3/server (or the React/Svelte equivalent) that calls createServer() and renderToString().

Running the App

npm install
nimbus serve

Everything runs on one server. Vite dev server starts automatically in the background with HMR. Edit inertia/ files and see changes instantly.

Project Structure

project/
โ”œโ”€โ”€ inertia/              # Frontend (React/Vue/Svelte)
โ”‚   โ”œโ”€โ”€ app.tsx           # Entry point
โ”‚   โ”œโ”€โ”€ layouts/          # Shared layouts
โ”‚   โ””โ”€โ”€ pages/            # Page components
โ”‚       โ”œโ”€โ”€ Dashboard/
โ”‚       โ”‚   โ””โ”€โ”€ Index.tsx
โ”‚       โ””โ”€โ”€ Users/
โ”‚           โ”œโ”€โ”€ Index.tsx
โ”‚           โ””โ”€โ”€ Create.tsx
โ”œโ”€โ”€ resources/views/
โ”‚   โ””โ”€โ”€ inertia_layout.nimbus  # Root HTML template
โ”œโ”€โ”€ public/build/              # Vite output (production)
โ””โ”€โ”€ ...

Plugin Capabilities

CapabilityDescription
HasMiddlewareInertia protocol (version negotiation, XHR detection)
HasConfigDefault configuration values
HasViewsEmbedded fallback root template
HasBindingsRegisters the Inertia manager in the IoC container

See also: Inertia Setup Guide, HMR Configuration, Unpoly (server-rendered alternative).