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 seperti air atau nodemon untuk 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 --> G

Cara 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:#fff

Perbedaan 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
    end

Session 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" --> H

Ringkasan #

  • 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.Controller bisa menjadi controller; method publik yang mengembalikan revel.Result otomatis menjadi action.
  • Interceptor bukan middleware — gunakan revel.InterceptMethod / revel.InterceptFunc di init.go untuk 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.

← Sebelumnya: Echo   Berikutnya: Selenium →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact