Revel #
Revel adalah satu-satunya full-stack web framework di ekosistem Go yang mengadopsi pola MVC (Model-View-Controller) secara penuh — mirip Ruby on Rails atau Django, bukan sekadar HTTP router. Revel hadir dengan CLI-nya sendiri, sistem routing berbasis file konfigurasi, hot-reload saat development, template engine bawaan, validasi terintegrasi, dan job scheduler. Ini berarti Revel cocok untuk kamu yang ingin membangun aplikasi web dengan server-side rendering dan konvensi yang sudah ditetapkan, bukan untuk membangun REST API murni. Artikel ini membahas cara kerja Revel dari awal: instalasi, struktur proyek, routing deklaratif, controller, interceptor, validasi, hingga kapan Revel adalah pilihan yang tepat dan kapan kamu sebaiknya memilih framework lain.
Instalasi #
Revel membutuhkan CLI tersendiri untuk membuat dan menjalankan project:
# Install Revel CLI
go install github.com/revel/cmd/revel@latest
# Verifikasi instalasi
revel version
Buat project baru:
revel new myapp
cd myapp
Jalankan development server dengan hot-reload:
revel run myapp
# Server berjalan di http://localhost:9000
Revel menggunakan hot-reload secara default saat development — perubahan kode langsung terlihat tanpa perlu restart server. Ini berbeda dengan Gin, Fiber, dan Echo yang membutuhkan tools terpisah sepertiairataunodemonuntuk efek serupa.
Struktur Project Revel #
Berbeda dari tiga framework sebelumnya yang memberi kebebasan penuh soal struktur direktori, Revel memaksakan struktur direktori yang spesifik. Ini adalah tradeoff antara konvensi dan fleksibilitas.
myapp/
├── app/
│ ├── controllers/ ← semua controller di sini
│ │ └── app.go
│ ├── models/ ← struct model / domain
│ │ └── user.go
│ ├── views/ ← template HTML
│ │ ├── App/
│ │ │ └── Index.html
│ │ └── errors/
│ │ ├── 404.html
│ │ └── 500.html
│ └── init.go ← inisialisasi aplikasi
├── conf/
│ ├── app.conf ← konfigurasi utama (port, database, dll)
│ └── routes ← definisi routing (BUKAN kode Go)
├── messages/ ← file i18n
├── public/ ← static assets (JS, CSS, gambar)
│ ├── css/
│ ├── js/
│ └── images/
└── tests/ ← integration tests
└── apptest.go
graph TD
A["conf/routes\n(deklarasi routing)"] --> B["app/controllers/\n(logika handler)"]
B --> C["app/models/\n(domain & data)"]
B --> D["app/views/\n(template HTML)"]
E["conf/app.conf\n(konfigurasi)"] --> B
F["public/\n(static assets)"] --> G["Browser"]
D --> GCara Kerja Request di Revel #
Alur request di Revel lebih panjang dari framework lain karena melibatkan beberapa lapisan konvensi MVC:
flowchart TD
A([HTTP Request]) --> B["conf/routes\ncocokkan URL ke Controller#Action"]
B --> C{Route ditemukan?}
C -- Tidak --> D["views/errors/404.html"]
C -- Ya --> E["Before Interceptor\n(BeforeRequest)"]
E --> F["Controller Action\n(method Go)"]
F --> G{Return type?}
G -- "render.Template" --> H["Template Engine\nrender views/Controller/Action.html"]
G -- "render.JSON" --> I["JSON Response"]
G -- "render.Redirect" --> J["HTTP Redirect"]
H --> K([Response ke Client])
I --> K
J --> K
D --> K
F --> L["After Interceptor\n(AfterRequest)"]
L --> K
style A fill:#7c3aed,color:#fff
style K fill:#7c3aed,color:#fff
style D fill:#e05252,color:#fffPerbedaan mendasar dari Gin/Fiber/Echo: di Revel, routing tidak didefinisikan dalam kode Go melainkan dalam file teks conf/routes, dan handler bukan fungsi biasa melainkan method pada struct controller.
Routing Deklaratif #
Routing Revel didefinisikan di conf/routes — sebuah file teks dengan format khusus, bukan kode Go. Ini adalah salah satu karakteristik paling berbeda dari Revel.
# conf/routes
# Format: METHOD PATH Controller.Action
# Halaman utama
GET / App.Index
# User routes
GET /users Users.List
GET /users/:id Users.Show
POST /users Users.Create
PUT /users/:id Users.Update
DELETE /users/:id Users.Delete
# Static assets — ditangani otomatis oleh Revel
GET /public/*filepath Static.Serve("public")
# Catch-all untuk 404
* /:controller/:action :controller.:action
Keunggulan dan Keterbatasan Routing Deklaratif #
Keunggulan:
✓ Semua route bisa dilihat sekaligus di satu file
✓ Non-developer (desainer, PM) bisa membaca dan memahami routing
✓ Mudah di-audit untuk keamanan
Keterbatasan:
✗ Tidak ada type checking dari compiler Go
✗ Typo pada nama Controller.Action baru ketahuan saat runtime
✗ Tidak bisa menggunakan logika kondisional dalam definisi route
Reverse Routing #
Revel meng-generate fungsi helper untuk membangun URL berdasarkan nama controller dan action, sehingga kamu tidak perlu hardcode path di template atau kode:
// Menghasilkan URL untuk Users.Show dengan id=42
url := c.ReverseOf(controllers.Users{}.Show, 42)
// Hasil: "/users/42"
Controller #
Controller di Revel adalah struct yang meng-embed revel.Controller. Setiap method publik yang mengembalikan revel.Result otomatis bisa menjadi action — tidak perlu registrasi manual.
Struktur Controller Dasar #
// app/controllers/users.go
package controllers
import (
"myapp/app/models"
"github.com/revel/revel"
)
type Users struct {
revel.Controller
}
// GET /users
func (c Users) List() revel.Result {
users := models.GetAllUsers()
return c.Render(users) // render views/Users/List.html
}
// GET /users/:id
func (c Users) Show(id int) revel.Result {
user := models.GetUserByID(id)
if user == nil {
return c.NotFound("User tidak ditemukan")
}
return c.Render(user) // render views/Users/Show.html
}
// POST /users
func (c Users) Create() revel.Result {
var user models.User
c.Params.BindJSON(&user)
if c.Validation.Required(user.Name).Message("Nama wajib diisi"); c.Validation.HasErrors() {
c.Validation.Keep()
c.FlashParams()
return c.Redirect(Users.Index)
}
models.SaveUser(&user)
c.Flash.Success("User berhasil dibuat")
return c.Redirect(Users.List)
}
Parameter Binding Otomatis #
Revel melakukan binding parameter secara otomatis berdasarkan nama parameter method — tidak perlu memanggil c.Param("id") secara eksplisit:
// Route: GET /users/:id
// Parameter "id" di path otomatis di-bind ke parameter "id" pada method
func (c Users) Show(id int) revel.Result {
// id sudah bertipe int, bukan string
// Revel melakukan konversi tipe secara otomatis
user := models.GetUserByID(id)
return c.Render(user)
}
// Route: GET /products/:slug
func (c Products) Detail(slug string) revel.Result {
product := models.GetProductBySlug(slug)
return c.Render(product)
}
// Query params juga di-bind otomatis
// GET /search?q=golang&page=2
func (c Search) Index(q string, page int) revel.Result {
results := models.Search(q, page)
return c.Render(results)
}
flowchart LR
A["conf/routes\nGET /users/:id Users.Show"] --> B["Revel Router"]
B --> C{Cocokkan parameter}
C -->|":id = '42'"| D["Konversi tipe\nstring → int"]
D --> E["Users.Show(id int)\nid = 42"]
E --> F["c.Render(user)\nviews/Users/Show.html"]Return Types (Result) #
Action Revel mengembalikan revel.Result — sebuah interface yang bisa berupa render template, JSON, redirect, atau lainnya:
// Render template — views/ControllerName/ActionName.html
// Variabel yang di-pass ke Render tersedia di template
func (c Users) List() revel.Result {
users := getAllUsers()
return c.Render(users)
// Template bisa mengakses variable "users" langsung
}
// Render JSON — untuk API endpoint
func (c Users) ListJSON() revel.Result {
users := getAllUsers()
return c.RenderJSON(users)
}
// Render XML
func (c Users) ListXML() revel.Result {
users := getAllUsers()
return c.RenderXML(users)
}
// Redirect ke action lain
func (c Users) Create() revel.Result {
// setelah simpan...
return c.Redirect(Users.List)
}
// Redirect ke URL
func (c App) OldPage() revel.Result {
return c.Redirect("/new-url")
}
// Error responses
func (c Users) Show(id int) revel.Result {
user := getUser(id)
if user == nil {
return c.NotFound("User tidak ditemukan")
}
return c.Render(user)
}
// Render string langsung
func (c App) Ping() revel.Result {
return c.RenderText("pong")
}
Template Engine #
Revel menggunakan Go’s html/template yang diperluas dengan fungsi-fungsi tambahan. Template disimpan di app/views/ dengan konvensi penamaan ControllerName/ActionName.html.
Konvensi Penamaan Template #
app/views/
├── Users/
│ ├── List.html ← untuk Users.List
│ ├── Show.html ← untuk Users.Show
│ └── Edit.html ← untuk Users.Edit
├── Products/
│ └── Index.html ← untuk Products.Index
└── errors/
├── 404.html
└── 500.html
Contoh Template #
<!-- app/views/Users/List.html -->
{{set . "title" "Daftar User"}}
{{template "header.html" .}}
<div class="container">
<h1>Daftar User</h1>
{{if .users}}
<table>
<thead>
<tr>
<th>Nama</th>
<th>Email</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
{{range .users}}
<tr>
<td>{{.Name}}</td>
<td>{{.Email}}</td>
<td>
<a href="{{url "Users.Show" .ID}}">Detail</a>
<a href="{{url "Users.Edit" .ID}}">Edit</a>
</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<p>Belum ada user.</p>
{{end}}
</div>
{{template "footer.html" .}}
Fungsi Template Bawaan Revel #
<!-- URL generation dari nama route -->
<a href="{{url "Users.Show" .ID}}">Detail</a>
<!-- Flash message -->
{{if .flash.success}}
<div class="alert alert-success">{{.flash.success}}</div>
{{end}}
<!-- Pesan validasi error -->
{{if .errors}}
<ul>
{{range .errors}}
<li>{{.Message}}</li>
{{end}}
</ul>
{{end}}
<!-- Format tanggal -->
<span>{{.CreatedAt | date "2006-01-02"}}</span>
<!-- Escape HTML (otomatis di html/template) -->
<p>{{.Description}}</p>
Validasi #
Revel memiliki sistem validasi bawaan yang terintegrasi langsung ke dalam controller via c.Validation:
func (c Users) Create() revel.Result {
var name string
var email string
var age int
c.Params.Bind(&name, "name")
c.Params.Bind(&email, "email")
c.Params.Bind(&age, "age")
// Aturan validasi
c.Validation.Required(name).
Message("Nama wajib diisi")
c.Validation.MinSize(name, 2).
Message("Nama minimal 2 karakter")
c.Validation.Email(email).
Message("Format email tidak valid")
c.Validation.Range(age, 18, 120).
Message("Usia harus antara 18 dan 120")
// Cek apakah ada error
if c.Validation.HasErrors() {
// Simpan error ke flash agar tersedia setelah redirect
c.Validation.Keep()
c.FlashParams()
return c.Redirect(Users.New)
}
// Simpan ke database
user := models.User{Name: name, Email: email, Age: age}
models.SaveUser(&user)
c.Flash.Success("User " + name + " berhasil dibuat!")
return c.Redirect(Users.List)
}
Validator yang Tersedia #
// String
c.Validation.Required(value) // tidak boleh kosong
c.Validation.MinSize(value, min) // panjang minimal
c.Validation.MaxSize(value, max) // panjang maksimal
c.Validation.Length(value, n) // panjang tepat n
c.Validation.Match(value, regexp) // cocok regex
c.Validation.Email(value) // format email valid
// Angka
c.Validation.Min(value, min) // nilai minimal
c.Validation.Max(value, max) // nilai maksimal
c.Validation.Range(value, min, max) // antara min dan max
// Umum
c.Validation.Required(value) // tidak nil/kosong/zero
flowchart TD
A["c.Params.Bind\n(ambil data dari request)"] --> B["c.Validation.Required / Email / Range / ..."]
B --> C{"c.Validation.HasErrors()?"}
C -- Ya --> D["c.Validation.Keep()\nc.FlashParams()"]
D --> E["c.Redirect ke form\nError tampil di template"]
C -- Tidak --> F["Simpan ke database"]
F --> G["c.Flash.Success(...)"]
G --> H["c.Redirect ke halaman list"]Interceptor #
Interceptor di Revel adalah ekuivalen middleware, tetapi lebih terikat ke controller. Ada tiga titik eksekusi: BEFORE (sebelum action), AFTER (setelah action), dan PANIC (saat panic terjadi).
Mendefinisikan Interceptor #
// app/controllers/auth.go
package controllers
import "github.com/revel/revel"
type Auth struct {
revel.Controller
}
// Interceptor yang dipanggil sebelum semua action pada controller apapun
// yang didaftarkan di init.go
func checkAuth(c *revel.Controller) revel.Result {
// Cek session
if _, ok := c.Session["userID"]; !ok {
return c.Redirect(Auth.Login)
}
return nil // nil berarti lanjutkan ke action
}
// Interceptor khusus untuk controller tertentu
func (c Users) checkOwnership() revel.Result {
userID := c.Session["userID"]
paramID := c.Params.Get("id")
if userID != paramID {
return c.Forbidden("Kamu tidak berhak mengakses resource ini")
}
return nil
}
Mendaftarkan Interceptor #
// app/init.go
package app
import "github.com/revel/revel"
func init() {
// BEFORE: checkAuth berjalan sebelum semua action di controllers.Users
revel.InterceptMethod(controllers.Users.checkAuth, revel.BEFORE)
// BEFORE: checkAuth berjalan sebelum semua action di controllers.Products
revel.InterceptMethod(controllers.Products.checkAuth, revel.BEFORE)
// BEFORE: berlaku untuk semua controller (gunakan fungsi, bukan method)
revel.InterceptFunc(checkLogin, revel.BEFORE, &controllers.Users{})
// AFTER: logging setelah semua action
revel.InterceptFunc(logRequest, revel.AFTER, revel.ALL_CONTROLLERS)
// PANIC: tangani panic
revel.InterceptFunc(handlePanic, revel.PANIC, revel.ALL_CONTROLLERS)
}
sequenceDiagram
participant Client
participant Router
participant BI as BEFORE Interceptor
participant Action as Controller Action
participant AI as AFTER Interceptor
Client->>Router: HTTP Request
Router->>BI: InterceptBEFORE dipanggil
BI->>BI: Validasi session / auth
alt Interceptor mengembalikan Result (non-nil)
BI-->>Client: Redirect / Error (action TIDAK dijalankan)
else Interceptor mengembalikan nil
BI->>Action: Lanjutkan ke action
Action->>Action: Proses bisnis
Action->>AI: InterceptAFTER dipanggil
AI->>AI: Logging, cleanup
AI-->>Client: Response dari action
endSession dan Flash #
Revel menyediakan session berbasis cookie yang di-sign dengan HMAC dan flash message untuk komunikasi antar redirect.
Session #
// Menyimpan ke session
func (c Users) Login() revel.Result {
// ... validasi credentials
c.Session["userID"] = strconv.Itoa(user.ID)
c.Session["userRole"] = user.Role
c.Session.SetNoExpiration() // atau SetDefaultExpiration()
return c.Redirect(App.Index)
}
// Membaca dari session
func (c Users) Profile() revel.Result {
userID, ok := c.Session["userID"]
if !ok {
return c.Redirect(Users.Login)
}
id, _ := strconv.Atoi(userID)
user := models.GetUserByID(id)
return c.Render(user)
}
// Menghapus session (logout)
func (c Users) Logout() revel.Result {
for k := range c.Session {
delete(c.Session, k)
}
return c.Redirect(App.Index)
}
Flash Message #
Flash message adalah data sementara yang bertahan hanya untuk satu request berikutnya — ideal untuk pesan sukses/error setelah redirect:
// Menyimpan flash
func (c Users) Create() revel.Result {
// ...setelah simpan berhasil
c.Flash.Success("User berhasil dibuat!")
c.Flash.Error("Gagal mengirim email konfirmasi.")
return c.Redirect(Users.List)
}
// Di template — flash otomatis tersedia
<!-- Template membaca flash otomatis dari .flash -->
{{if .flash.success}}
<div class="alert alert-success">{{.flash.success}}</div>
{{end}}
{{if .flash.error}}
<div class="alert alert-danger">{{.flash.error}}</div>
{{end}}
Konfigurasi #
Revel menggunakan file conf/app.conf dengan format key=value yang mendukung multiple environment (dev, test, prod):
# conf/app.conf
# Konfigurasi aplikasi dasar
app.name = MyApp
app.secret = ganti-dengan-string-acak-yang-panjang-dan-aman
# HTTP server
http.addr =
http.port = 9000
http.ssl = false
# Mode aktif (dev, test, prod)
mode.dev = true
# ────────────────────────────────────────
# Override per environment — awali dengan nama mode
# ────────────────────────────────────────
# Development
dev.results.pretty = true
dev.log.level = debug
dev.db.driver = sqlite3
dev.db.spec = myapp_dev.db
# Production
prod.results.pretty = false
prod.log.level = warn
prod.http.port = 80
prod.db.driver = postgres
prod.db.spec = host=db user=app password=secret dbname=myapp sslmode=require
Membaca konfigurasi di dalam kode:
// Membaca nilai konfigurasi
dbDriver, _ := revel.Config.String("db.driver")
dbSpec, _ := revel.Config.String("db.spec")
port, _ := revel.Config.Int("http.port")
isPretty, _ := revel.Config.Bool("results.pretty")
// Dengan default value
timeout, _ := revel.Config.IntDefault("http.timeout", 30)
logLevel := revel.Config.StringDefault("log.level", "info")
Job Scheduler #
Revel menyertakan job scheduler berbasis cron untuk menjalankan task terjadwal — sesuatu yang harus kamu setup secara manual di framework lain:
go get github.com/revel/modules/jobs/app/jobs
// app/jobs/cleanup.go
package jobs
import "github.com/revel/modules/jobs/app/jobs"
type CleanupJob struct{}
func (j CleanupJob) Run() {
// Hapus session yang kedaluwarsa
models.CleanExpiredSessions()
revel.AppLog.Info("Cleanup session selesai")
}
// app/init.go
func init() {
// Jalankan setiap hari pukul 02:00
jobs.Schedule("0 2 * * ?", CleanupJob{})
// Jalankan setiap 30 menit
jobs.Every(30*time.Minute, CleanupJob{})
// Jalankan sekali saat aplikasi start
jobs.Now(SeedDataJob{})
}
Testing #
Revel menyediakan framework testing terintegrasi via revel.TestSuite:
// tests/usertest.go
package tests
import (
"github.com/revel/revel/testing"
)
type UserTest struct {
testing.TestSuite
}
func (t *UserTest) Before() {
println("Setup sebelum setiap test")
}
func (t *UserTest) TestCreateUser() {
t.Post("/users", "application/json",
strings.NewReader(`{"name":"Uni","email":"[email protected]"}`))
t.AssertStatus(201)
t.AssertContentType("application/json")
}
func (t *UserTest) TestGetUser() {
t.Get("/users/1")
t.AssertStatus(200)
t.AssertContains("Uni")
}
func (t *UserTest) TestUserNotFound() {
t.Get("/users/99999")
t.AssertStatus(404)
}
func (t *UserTest) After() {
println("Teardown setelah setiap test")
}
Jalankan test:
revel test myapp dev
Kapan Tidak Menggunakan Revel #
Revel adalah framework yang sangat opinionated. Penting untuk memahami tradeoff-nya sebelum memilihnya:
Gunakan Revel jika:
✓ Membangun aplikasi web dengan server-side rendering (SSR)
✓ Tim menginginkan konvensi MVC yang ketat ala Rails/Django
✓ Butuh hot-reload, job scheduler, dan validasi dalam satu package
✓ Kecepatan development awal lebih penting dari fleksibilitas arsitektur
✓ Tidak butuh performa raw yang ekstrem
Jangan gunakan Revel jika:
✗ Membangun REST API murni untuk dikonsumsi frontend SPA atau mobile
→ Gin, Fiber, atau Echo jauh lebih sesuai
✗ Butuh fleksibilitas penuh dalam struktur proyek
→ Revel memaksa struktur direktori yang tidak bisa diubah
✗ Performa adalah prioritas utama
→ Overhead MVC dan template engine menambah latency
✗ Tim berencana menggunakan library net/http standar secara ekstensif
→ Revel punya ekosistemnya sendiri yang terpisah
✗ Project berskala microservice kecil
→ Revel terlalu berat untuk service yang hanya punya 3-5 endpoint
flowchart TD
A{Apa yang kamu bangun?} --> B["Aplikasi web\ndengan HTML rendering"]
A --> C["REST API /\nJSON service"]
B --> D{Butuh konvensi\nMVC ketat?}
D -- Ya --> E["✓ Revel"]
D -- Tidak --> F["Echo / Gin dengan\ntemplate engine manual"]
C --> G{Prioritas utama?}
G -- Performa --> H["✓ Fiber"]
G -- "Ekosistem\nnet/http" --> I["✓ Echo atau Gin"]
G -- "Familier\nExpress.js" --> HRingkasan #
- Full-stack MVC — Revel adalah satu-satunya framework Go yang mengadopsi MVC secara penuh dengan CLI, hot-reload, template engine, validasi, session, dan job scheduler terintegrasi.
- Routing deklaratif — route didefinisikan di
conf/routes(bukan kode Go); keseluruhan routing bisa dilihat sekaligus tetapi tanpa type checking compiler.- Parameter binding otomatis — nama parameter method Go di-match langsung ke path param dan query string; Revel melakukan konversi tipe secara otomatis.
- Controller sebagai struct — setiap struct yang meng-embed
revel.Controllerbisa menjadi controller; method publik yang mengembalikanrevel.Resultotomatis menjadi action.- Interceptor bukan middleware — gunakan
revel.InterceptMethod/revel.InterceptFuncdiinit.gountuk logika yang berjalan sebelum/sesudah action; lebih terikat ke controller dibanding middleware di framework lain.- Session cookie HMAC — session disimpan di cookie yang di-sign secara kriptografis; tidak butuh server-side session store untuk kasus umum.
- Flash message — data sementara yang bertahan satu request berikutnya, sangat berguna untuk pesan sukses/error setelah redirect.
- Bukan pilihan untuk REST API murni — jika kamu membangun API JSON tanpa server-side rendering, Gin, Fiber, atau Echo akan jauh lebih sesuai dan efisien.