🕌

Goだけでフルスタック開発する新しい選択肢 ― Marionette

に公開

始めに

Webアプリ開発って、正直つらくないですか?

  • バックエンド(Go / Python / etc)
  • フロントエンド(React / Vue / etc)
  • API設計
  • 状態管理

「やりたいことはシンプルなのに、技術スタックが重すぎる」

そんな違和感から、Goだけで完結するフルスタックフレームワークを作りました。

https://github.com/YoshihideShirai/marionette

Marionetteとは

Marionetteは、

Goコードからフロントエンドを“操る”フルスタックフレームワーク

です。

名前の由来は「操り人形(Marionette)」
GoがUIを操作する様子が、そのままコンセプトになっています。

コンセプト

Marionetteの思想はかなりシンプルです。

✅ 1. Goだけで完結

  • フロントエンド
  • バックエンド
  • 状態管理
  • 通信

すべてGoで記述できます。

👉 フロントエンド言語不要

✅ 2. 宣言的UI(React風)

GoでUIをこう書けます:

mf.Stack(
    mf.PageHeader(...),
    mf.ActionForm(...),
    mf.Region(...),
)
  • JSXなし
  • TypeScript不要
  • でも構造はReactライク

✅ 3. htmxベースの部分更新

フルSPAではない

サーバー駆動UI

必要な部分だけ更新

👉 シンプルで高速

✅ 4. Hot Reload対応

開発時は変更が即反映されます。

何が嬉しいのか?

従来

項目 技術
フロント React
API Go / Node
状態管理 Redux
通信 REST / GraphQL

👉 分断される

Marionette

項目 技術
すべて Go

👉 1つの言語で統一

サンプル:タスク管理アプリ

インストール

go get github.com/YoshihideShirai/marionette@latest

main.go

package main

import (
	"strings"

	mb "github.com/YoshihideShirai/marionette/backend"
	mf "github.com/YoshihideShirai/marionette/frontend"
)

func main() {
	app := buildApp("Simple Tasks", "Marionette end-to-end sample")
	if err := app.Run("127.0.0.1:8081"); err != nil {
		panic(err)
	}
}

type task struct {
	ID   int
	Name string
}

func buildApp(title, description string) *mb.App {
	app := mb.New()
	app.SetGlobal("tasks", []task{})
	app.SetGlobal("nextID", 1)

	app.Page("/", func(ctx *mb.Context) mf.Node {
		return page(ctx, title, description)
	}, mb.WithTitle(title))

	app.Action("tasks/create", func(ctx *mb.Context) mf.Node {
		name := strings.TrimSpace(ctx.FormValue("name"))
		if name != "" {
			nextID := ctx.IncrementGlobalInt("nextID", 1) - 1
			ctx.UpdateGlobal("tasks", func(old any) any {
				tasks := append([]task(nil), old.([]task)...)
				return append(tasks, task{ID: nextID, Name: name})
			})
		}
		return taskList(ctx.GetGlobal("tasks").([]task))
	})

	return app
}

func page(ctx *mb.Context, title, description string) mf.Node {
	tasks := ctx.GetGlobal("tasks").([]task)
	return mf.Container(mf.ContainerProps{MaxWidth: "4xl", Centered: true},
		mf.Stack(mf.StackProps{Direction: "column", Gap: "6"},
			mf.PageHeader(mf.PageHeaderProps{
				Title:       title,
				Description: description,
			}),
			mf.ActionForm(mf.ActionFormProps{
				Action: "/tasks/create",
				Target: "#task-list",
				Swap:   "innerHTML",
				Props:  mf.ComponentProps{Class: "space-y-3"},
			},
				mf.FormRow(mf.FormRowProps{
					ID:       "task-name",
					Label:    "Task",
					Required: true,
					Control: mf.TextField(mf.TextFieldProps{
						ID:          "task-name",
						Name:        "name",
						Placeholder: "Task name",
						Required:    true,
					}),
				}),
				mf.SubmitButton("Add Task", mf.ComponentProps{}),
			),
			mf.Region(mf.RegionProps{ID: "task-list"}, taskList(tasks)),
		),
	)
}

func taskList(tasks []task) mf.Node {
	if len(tasks) == 0 {
		return mf.EmptyState(mf.EmptyStateProps{Title: "No tasks yet"})
	}

	rows := make([]mf.TableComponentRow, 0, len(tasks))
	for _, t := range tasks {
		rows = append(rows, mf.TableRowValues(t.ID, t.Name))
	}
	return mf.Table(mf.TableProps{
		Columns: []mf.TableColumn{{Label: "ID"}, {Label: "Name"}},
		Rows:    rows,
	})
}

ポイント解説

🧠 状態管理

app.Set("tasks", ...)
ctx.Get("tasks")

👉 サーバー側に状態を持つ

🔁 UI更新

app.Action("tasks/create", ...)

👉 アクション実行 → 部分更新

🧩 UI構築

mf.Container(
mf.Stack(...),
)

👉 完全にGoでUI構築

どんな用途に向いているか?

👍 向いている

  • 管理画面
  • 内製ツール
  • プロトタイプ
  • 小〜中規模アプリ
  • Streamlitの代替

🤔 向いていない

  • 大規模SPA
  • 超リッチUI
  • フロント分業前提のチーム

類似技術との比較

技術 特徴
Streamlit PythonでUI、でも自由度低い
React 高機能、でも重い
HTMX シンプルだが構造管理が弱い
Marionette Goで統一 + UI DSL

今後の展望

  • コンポーネント拡張
  • テンプレート機能
  • デプロイ周りの整備
  • WebView対応強化

まとめ

Marionetteは、

「GoだけでWebアプリを書きたい」

というニーズに対する、かなりストレートな解です。

  • フロントエンド 不要
  • API設計 不要
  • 状態管理も統一

👉 思考コストが劇的に下がる

おわりに

まだまだ発展途上ですが、
コンセプトとしてはかなり面白い領域だと思っています。

ぜひ触ってみてください👇

👉 https://github.com/YoshihideShirai/marionette


フィードバック・Issue・Starお待

Discussion