Inertia.js

Connect Go handlers to React components with Inertia.js for SPA navigation without building an API.

Inertia.js bridges Go handlers and React components, enabling SPA-like navigation without building an API.

Quick Start

// Handler
import (
    "github.com/velocitykode/velocity/pkg/router"
    "github.com/velocitykode/velocity/pkg/view"
)

func (c *UserHandler) Index(ctx *router.Context) error {
    users, _ := models.User{}.All()

    view.Render(ctx.Response, ctx.Request, "Users/Index", view.Props{
        "users": users,
        "title": "All Users",
    })

    return nil
}
// resources/js/pages/Users/Index.tsx
import { Link } from '@inertiajs/react'

interface Props {
  users: User[]
  title: string
}

export default function UsersIndex({ users, title }: Props) {
  return (
    <div>
      <h1>{title}</h1>
      {users.map(user => (
        <Link key={user.id} href={`/users/${user.id}`}>
          {user.name}
        </Link>
      ))}
    </div>
  )
}

Configuration

Initialize View System

import "github.com/velocitykode/velocity/pkg/view"

func main() {
    // Initialize with default template
    view.Initialize(view.Config{
        RootTemplate: view.DefaultTemplate,
        Version:      "1",
    })

    // Or load custom template
    template, _ := view.LoadTemplateFromFile("resources/views/app.html")
    view.Initialize(view.Config{
        RootTemplate: template,
        Version:      "1",
    })
}

Rendering Pages

Basic Rendering

import (
    "github.com/velocitykode/velocity/pkg/router"
    "github.com/velocitykode/velocity/pkg/view"
)

func (c *PostHandler) Index(ctx *router.Context) error {
    posts, _ := models.Post{}.With("User").Get()

    view.Render(ctx.Response, ctx.Request, "Posts/Index", view.Props{
        "posts": posts,
        "meta": map[string]interface{}{
            "title": "All Posts",
            "description": "Browse all posts",
        },
    })

    return nil
}

func (c *PostHandler) Show(ctx *router.Context) error {
    id := ctx.Param("id")
    post, err := models.Post{}.With("User", "Comments").Find(id)

    if err != nil {
        view.Render(ctx.Response, ctx.Request, "Errors/NotFound", view.Props{
            "message": "Post not found",
        })
        return nil
    }

    view.Render(ctx.Response, ctx.Request, "Posts/Show", view.Props{
        "post": post,
    })

    return nil
}

Shared Data

Share data across all views using middleware:

func ShareDataMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Share global data
        view.Share("app", map[string]interface{}{
            "name": "My Velocity App",
            "version": "1.0.0",
        })

        // Share authenticated user
        if auth.Check(r) {
            view.Share("auth", map[string]interface{}{
                "user": auth.User(r),
            })
        }

        next.ServeHTTP(w, r)
    })
}

Share request-specific data:

func (c *UserHandler) Index(ctx *router.Context) error {
    view.SetSharePropsFunc(func(r *http.Request) (view.Props, error) {
        return view.Props{
            "csrf_token": getCSRFToken(r),
            "flash": getFlashMessages(r),
        }, nil
    })

    users, _ := models.User{}.All()
    view.Render(ctx.Response, ctx.Request, "Users/Index", view.Props{
        "users": users,
    })

    return nil
}
func (c *PostHandler) Store(ctx *router.Context) error {
    post := createPost(ctx.Request)

    // Redirect to show page
    view.Location(ctx.Response, ctx.Request, fmt.Sprintf("/posts/%d", post.ID))

    return nil
}

func (c *PostHandler) Update(ctx *router.Context) error {
    if hasErrors {
        // Go back with errors
        view.Back(ctx.Response, ctx.Request)
        return nil
    }

    // Redirect on success
    view.Location(ctx.Response, ctx.Request, "/posts")

    return nil
}

Middleware Integration

import (
    "github.com/velocitykode/velocity/pkg/router"
    "github.com/velocitykode/velocity/pkg/view"
)

func SetupRoutes() *router.Router {
    r := router.New()

    // Apply Inertia middleware
    r.Use(view.Middleware())
    r.Use(ShareDataMiddleware)

    // Routes
    r.Get("/", homeHandler.Index)
    r.Get("/posts", postHandler.Index)
    r.Get("/posts/{id}", postHandler.Show)

    return r
}

Testing Views

func TestPostHandler_Index(t *testing.T) {
    req := httptest.NewRequest("GET", "/posts", nil)
    w := httptest.NewRecorder()
    ctx := router.NewContext(w, req)

    handler := &PostHandler{}
    err := handler.Index(ctx)

    // Assert HTML response
    assert.NoError(t, err)
    assert.Equal(t, http.StatusOK, w.Code)
    assert.Contains(t, w.Header().Get("Content-Type"), "text/html")

    // For Inertia requests, assert JSON
    req.Header.Set("X-Inertia", "true")
    w = httptest.NewRecorder()
    ctx = router.NewContext(w, req)
    err = handler.Index(ctx)

    assert.NoError(t, err)
    assert.Equal(t, http.StatusOK, w.Code)
    assert.Contains(t, w.Header().Get("Content-Type"), "application/json")
}