Steward: Admin Panel dengan Go Murni

Steward: Admin Panel dengan Go Murni

Daftar Isi

Setiap tool internal membutuhkan admin panel, dan menulisnya adalah pekerjaan paling tidak menarik dalam pengembangan perangkat lunak. Halaman daftar, form tambah, form ubah, halaman detail, konfirmasi hapus. Lalu validasi. Lalu hak akses. Lalu semuanya terulang lagi untuk tabel berikutnya.

PHP sudah menyelesaikan ini sejak lama. Laravel punya dcat-admin, Django punya admin bawaannya, dan keduanya memungkinkan Anda mendeskripsikan sebuah resource lalu memperoleh antarmuka yang langsung berfungsi.

Go belum benar-benar punya jawabannya. Anda menulis CRUD-nya sendiri, atau menempelkan SPA JavaScript pada API Anda, dan kini ada dua aplikasi beserta satu build toolchain yang harus dipelihara.

Maka saya membangun Steward: framework admin panel server-rendered untuk Go, mengambil bentuk dari dcat-admin lalu menyusunnya kembali di atas generics.

Bagaimana rasanya memakainya

Sebuah resource hanyalah satu struct dan beberapa pemanggilan builder:

posts := steward.Register[Post](app).Title("Posts").Icon("news")

posts.Grid(func(g *steward.Grid[Post]) {
    g.Column("Title").Limit(40).Sortable()
    g.Column("Status").Badge(map[any]string{"draft": "secondary", "published": "green"})
    g.QuickSearch("Title", "Body")
})

posts.Form(func(f *steward.Form[Post]) {
    f.Text("Title").Rules("required|max:255")
    f.Markdown("Body")
})

steward.Register[Post](app) saja sudah memberi Anda halaman daftar, tambah, ubah, detail, dan hapus yang berfungsi. Semua setelah itu hanyalah penyempurnaan.

Keputusan desain yang akan saya pertahankan

Generics, agar callback tidak pernah menyerahkan map

Inilah seluruh alasan proyek ini ada di Go, bukan di tempat lain.

Setiap callback builder menerima tipe model Anda yang sebenarnya. Grid[Post], Form[Post], hook yang menerima *Post. Bukan map[string]any, bukan interface{} beserta type assertion dan sedikit doa.

Jika Anda mengubah nama sebuah field, compiler akan menemukan setiap tempat yang merujuknya. Itulah nilai jual utamanya, dan itu pula hal yang secara struktural tidak bisa ditawarkan oleh framework admin bertipe dinamis.

Gagal saat boot, bukan saat diklik

Nama kolom berupa string. Dan string adalah tempat salah ketik bersemayam.

Karena itu Steward menjalankan Verify() saat startup, memeriksa setiap rujukan kolom di seluruh grid, form, dan filter terhadap skema yang telah diurai. Satu salah ketik langsung menghentikan proses dengan pesan yang jelas, alih-alih memunculkan error 500 saat seseorang pertama kali mengklik header pengurutan di produksi tiga pekan kemudian.

Saya lebih memilih startup yang berisik daripada hari Selasa yang tenang.

Satu binary, dan tanpa Node sama sekali

Antarmukanya memakai Tailwind v4, komponen Basecoat, dan htmx. Semuanya dikompilasi lebih dahulu menggunakan esbuild Go API dan Tailwind standalone binary, disimpan ke repository, lalu dikirimkan melalui go:embed.

Tidak ada Node saat build maupun saat runtime. Anda mendapat satu binary. Template tetap bisa ditimpa dengan menaruh satu file di proyek Anda sendiri.

Pilihan ini benar-benar berguna untuk jenis deployment yang biasa saya tangani: salin satu file ke server, lalu jalankan. Tidak ada toolchain yang perlu dipasang, tidak ada yang perlu disinkronkan.

Headless tanpa biaya tambahan

Setiap endpoint resource menyajikan JSON bila diminta dengan Accept: application/json, dari handler yang sama dengan yang merender HTML. Ada pula endpoint _schema, serta bearer token opsional agar script dan klien mobile bisa terautentikasi tanpa cookie maupun proses CSRF.

REST API di sini bukan fitur terpisah, melainkan content negotiation atas pekerjaan yang sudah dilakukan.

Migrasi, bukan AutoMigrate

AutoMigrate milik GORM memang praktis, tetapi diam-diam membuat skema Anda menyimpang. Steward menyertakan migrasi framework yang terversi beserta runner untuk migrasi Anda sendiri, sehingga isi basis data selaras dengan apa yang ada di version control.

Apa saja di dalamnya

RBAC dengan role, permission yang dicocokkan pada path HTTP, serta policy yang ditegakkan pada tingkat resource, baris, dan field, ditambah pengelola menu drag-and-drop yang diatur dari dalam panel itu sendiri.

Grid-nya mendukung kolom yang bisa diurutkan, DSL pencarian cepat (field:value, >n, %contains%), panel filter, filter lintas relasi satu tingkat melalui subquery agar hitungan paginasi tetap benar, paginasi berjendela, ekspor CSV, aksi massal, penyuntingan inline, tree grid, pemilih kolom, serta pengurutan baris dengan menggeser.

Form mencakup 22 jenis field, validasi deklaratif dengan aturan terpisah untuk tambah dan ubah, hook siklus hidup bertipe, select BelongsTo yang bisa dicari, sinkronisasi pivot MultiSelect, form baris hasMany bersarang, pembaruan hanya pada field yang berubah, serta field kondisional yang ditolak saat submit alih-alih sekadar disembunyikan. Ada pula pemilih ikon untuk seluruh set Lucide, digambar dari satu sprite ter-cache alih-alih menyisipkan seribu enam ratus SVG.

Ditambah widget dasbor, background worker berbasis cron pada proses terpisah dari panel, dan CLI scaffolding yang bisa menghasilkan resource dari spesifikasi field, basis data yang sedang berjalan, atau struct Go yang sudah ada.

Status sejujurnya

Pra-rilis, masih dalam pengembangan aktif, dan API-nya akan berubah. Saya akan memakainya untuk tool internal hari ini. Saya tidak akan menyarankan Anda membangun perusahaan Anda di atasnya bulan ini.

Daftar fitur di repository menandai apa yang sudah berfungsi menyeluruh dan apa yang masih dalam rencana, dan saya menjaga pembedaan itu tetap jujur alih-alih penuh harapan. Pengurutan berdasarkan kolom relasi membutuhkan join dan untuk sekarang ditolak saat boot, bukan setengah berjalan. Kolom yang dipatok dan tata letak form bertab belum tersedia.

Mengapa saya terus mengerjakannya

Terutama karena saya ingin ia ada. Setiap proyek Go yang saya bangun akhirnya membutuhkan admin panel, dan saya lelah menulis lima handler yang sama.

Ada alasan kedua. Membangun framework memaksa Anda memiliki pendirian tentang bahasa yang Anda pakai, dan generics di Go masih cukup baru sehingga ergonominya belum mapan. Menggali seperti apa rasanya API builder bertipe yang baik telah mengajarkan saya lebih banyak tentang Go dibanding berapa pun banyaknya kode aplikasi.

Baca dokumentasinya atau lihat kodenya di GitHub. Gratis dan open source, dan saya sungguh ingin mendengar di bagian mana ia patah.

call to action

Siap membangun proyek bersama saya?

Saya siap membantu membangun, mengembangkan, dan meluncurkan proyek Anda berikutnya — kirim pesan sekarang dan ayo kita mulai!

Mulai Sekarang