Revel #

Revel is the only full-stack web framework in the Go ecosystem that fully adopts the MVC (Model-View-Controller) pattern — like Ruby on Rails or Django, not just an HTTP router. Revel comes with its own CLI, a file-based routing system, hot-reload during development, a built-in template engine, integrated validation, and a job scheduler. This means Revel fits developers who want to build web applications with server-side rendering and established conventions, not those building pure REST APIs. This article covers how Revel works from the ground up: installation, project structure, declarative routing, controllers, interceptors, validation, and when Revel is the right choice versus when you should pick another framework.

Installation #

Revel needs its own CLI to create and run projects:

# Install the Revel CLI
go install github.com/revel/cmd/revel@latest

# Verify the installation
revel version

Create a new project:

revel new myapp
cd myapp

Run the development server with hot-reload:

revel run myapp
# The server runs at http://localhost:9000
Revel uses hot-reload by default during development — code changes are immediately visible without restarting the server. This differs from Gin, Fiber, and Echo, which need separate tools like air or nodemon for a similar effect.

Revel Project Structure #

Unlike the previous three frameworks, which give you full freedom over directory structure, Revel enforces a specific directory layout. This is the tradeoff between convention and flexibility.

myapp/
  ├── app/
  │   ├── controllers/         ← all controllers go here
  │   │   └── app.go
  │   ├── models/              ← model / domain structs
  │   │   └── user.go
  │   ├── views/               ← HTML templates
  │   │   ├── App/
  │   │   │   └── Index.html
  │   │   └── errors/
  │   │       ├── 404.html
  │   │       └── 500.html
  │   └── init.go              ← application initialization
  ├── conf/
  │   ├── app.conf             ← main configuration (port, database, etc.)
  │   └── routes               ← route definitions (NOT Go code)
  ├── messages/                ← i18n files
  ├── public/                  ← static assets (JS, CSS, images)
  │   ├── css/
  │   ├── js/
  │   └── images/
  └── tests/                   ← integration tests
      └── apptest.go
graph TD
    A["conf/routes\n(route declarations)"] --> B["app/controllers/\n(handler logic)"]
    B --> C["app/models/\n(domain & data)"]
    B --> D["app/views/\n(HTML templates)"]
    E["conf/app.conf\n(configuration)"] --> B
    F["public/\n(static assets)"] --> G["Browser"]
    D --> G

How Requests Work in Revel #

The request flow in Revel is longer than in other frameworks because it involves several layers of MVC conventions:

flowchart TD
    A([HTTP Request]) --> B["conf/routes\nmatch URL to Controller#Action"]
    B --> C{Route found?}
    C -- No --> D["views/errors/404.html"]
    C -- Yes --> E["Before Interceptor\n(BeforeRequest)"]
    E --> F["Controller Action\n(Go method)"]
    F --> G{Return type?}
    G -- "render.Template" --> H["Template Engine\nrenders views/Controller/Action.html"]
    G -- "render.JSON" --> I["JSON Response"]
    G -- "render.Redirect" --> J["HTTP Redirect"]
    H --> K([Response to the Client])
    I --> K
    J --> K
    D --> K
    F --> L["After Interceptor\n(AfterRequest)"]
    L --> K

Revel MVC Components vs Go Standard #

MVC ComponentRevel ImplementationStandard Go (net/http) Equivalent
ModelOrdinary Go structs under the app/models/ subfolderGo structs, DB entities, or DTOs
ViewHTML templates (html/template) in the app/views/ folderhtml/template.ParseFiles / manual HTML writing
ControllerGo structs embedding *revel.Controller in app/controllers/HTTP Handler functions (http.HandlerFunc)
RoutingA separate declarative text file at conf/routesMultiplexer/routing setup in main.go using code

The fundamental difference from Gin/Fiber/Echo: in Revel, routing isn’t defined in Go code but in the conf/routes text file, and handlers aren’t plain functions but methods on controller structs.


Declarative Routing #

Revel routing is defined in conf/routes — a text file with a special format, not Go code. This is one of Revel’s most distinctive characteristics.

# conf/routes
# Format: METHOD  PATH  Controller.Action

# Home page
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 — handled automatically by Revel
GET     /public/*filepath   Static.Serve("public")

# Catch-all for 404s
*       /:controller/:action  :controller.:action

Advantages and Limitations of Declarative Routing #

Advantages:
  ✓ All routes can be viewed at once in a single file
  ✓ Non-developers (designers, PMs) can read and understand the routing
  ✓ Easy to audit for security

Limitations:
  ✗ No compiler type checking
  ✗ Typos in Controller.Action names are only caught at runtime
  ✗ Can't use conditional logic in route definitions

Reverse Routing #

Revel generates helper functions to build URLs from controller and action names, so you don’t need to hardcode paths in templates or code:

// Generates the URL for Users.Show with id=42
url := c.ReverseOf(controllers.Users{}.Show, 42)
// Result: "/users/42"

Controllers #

A Revel controller is a struct that embeds revel.Controller. Every public method returning revel.Result automatically becomes an action — no manual registration needed.

Basic Controller Structure #

// 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) // renders 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 not found")
    }
    return c.Render(user) // renders 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("Name is required"); c.Validation.HasErrors() {
        c.Validation.Keep()
        c.FlashParams()
        return c.Redirect(Users.Index)
    }

    models.SaveUser(&user)
    c.Flash.Success("User created successfully")
    return c.Redirect(Users.List)
}

Automatic Parameter Binding #

Revel binds parameters automatically based on method parameter names — no need to call c.Param("id") explicitly:

// Route: GET /users/:id
// The "id" path parameter is automatically bound to the "id" method parameter
func (c Users) Show(id int) revel.Result {
    // id is already an int, not a string
    // Revel performs the type conversion automatically
    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 are also bound automatically
// 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{Match parameters}
    C -->|":id = '42'"| D["Type conversion\nstring → int"]
    D --> E["Users.Show(id int)\nid = 42"]
    E --> F["c.Render(user)\nviews/Users/Show.html"]

Return Types (Results) #

Revel actions return revel.Result — an interface that can be a template render, JSON, redirect, or more:

// Render a template — views/ControllerName/ActionName.html
// Variables passed to Render are available in the template
func (c Users) List() revel.Result {
    users := getAllUsers()
    return c.Render(users)
    // The template can access the "users" variable directly
}

// Render JSON — for API endpoints
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 to another action
func (c Users) Create() revel.Result {
    // after saving...
    return c.Redirect(Users.List)
}

// Redirect to a 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 not found")
    }
    return c.Render(user)
}

// Render a string directly
func (c App) Ping() revel.Result {
    return c.RenderText("pong")
}

Template Engine #

Revel uses Go’s html/template extended with additional functions. Templates live in app/views/ with the ControllerName/ActionName.html naming convention.

Template Naming Convention #

app/views/
  ├── Users/
  │   ├── List.html      ← for Users.List
  │   ├── Show.html      ← for Users.Show
  │   └── Edit.html      ← for Users.Edit
  ├── Products/
  │   └── Index.html     ← for Products.Index
  └── errors/
      ├── 404.html
      └── 500.html

Example Template #

<!-- app/views/Users/List.html -->
{{set . "title" "User List"}}
{{template "header.html" .}}

<div class="container">
    <h1>User List</h1>

    {{if .users}}
    <table>
        <thead>
            <tr>
                <th>Name</th>
                <th>Email</th>
                <th>Actions</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>No users yet.</p>
    {{end}}
</div>

{{template "footer.html" .}}

Revel’s Built-in Template Functions #

<!-- URL generation from route names -->
<a href="{{url "Users.Show" .ID}}">Detail</a>

<!-- Flash messages -->
{{if .flash.success}}
<div class="alert alert-success">{{.flash.success}}</div>
{{end}}

<!-- Validation error messages -->
{{if .errors}}
<ul>
    {{range .errors}}
    <li>{{.Message}}</li>
    {{end}}
</ul>
{{end}}

<!-- Date formatting -->
<span>{{.CreatedAt | date "2006-01-02"}}</span>

<!-- HTML escaping (automatic in html/template) -->
<p>{{.Description}}</p>

Validation #

Revel has a built-in validation system integrated directly into controllers 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")

    // Validation rules
    c.Validation.Required(name).
        Message("Name is required")

    c.Validation.MinSize(name, 2).
        Message("Name must be at least 2 characters")

    c.Validation.Email(email).
        Message("Invalid email format")

    c.Validation.Range(age, 18, 120).
        Message("Age must be between 18 and 120")

    // Check for errors
    if c.Validation.HasErrors() {
        // Store the errors in flash so they're available after the redirect
        c.Validation.Keep()
        c.FlashParams()
        return c.Redirect(Users.New)
    }

    // Save to the database
    user := models.User{Name: name, Email: email, Age: age}
    models.SaveUser(&user)

    c.Flash.Success("User " + name + " created successfully!")
    return c.Redirect(Users.List)
}

Available Validators #

// String
c.Validation.Required(value)           // must not be empty
c.Validation.MinSize(value, min)       // minimum length
c.Validation.MaxSize(value, max)       // maximum length
c.Validation.Length(value, n)          // exact length n
c.Validation.Match(value, regexp)      // regex match
c.Validation.Email(value)              // valid email format

// Numbers
c.Validation.Min(value, min)           // minimum value
c.Validation.Max(value, max)           // maximum value
c.Validation.Range(value, min, max)    // between min and max

// General
c.Validation.Required(value)           // not nil/empty/zero
flowchart TD
    A["c.Params.Bind\n(get data from the request)"] --> B["c.Validation.Required / Email / Range / ..."]
    B --> C{"c.Validation.HasErrors()?"}
    C -- Yes --> D["c.Validation.Keep()\nc.FlashParams()"]
    D --> E["c.Redirect to the form\nErrors shown in the template"]
    C -- No --> F["Save to the database"]
    F --> G["c.Flash.Success(...)"]
    G --> H["c.Redirect to the list page"]

Interceptors #

Interceptors in Revel are the equivalent of middleware, but more tied to controllers. There are three execution points: BEFORE (before the action), AFTER (after the action), and PANIC (when a panic occurs).

Defining Interceptors #

// app/controllers/auth.go
package controllers

import "github.com/revel/revel"

type Auth struct {
    revel.Controller
}

// Interceptor called before all actions on any controller
// registered in init.go
func checkAuth(c *revel.Controller) revel.Result {
    // Check the session
    if _, ok := c.Session["userID"]; !ok {
        return c.Redirect(Auth.Login)
    }
    return nil // nil means continue to the action
}

// Interceptor specific to a controller
func (c Users) checkOwnership() revel.Result {
    userID := c.Session["userID"]
    paramID := c.Params.Get("id")

    if userID != paramID {
        return c.Forbidden("You are not allowed to access this resource")
    }
    return nil
}

Registering Interceptors #

// app/init.go
package app

import "github.com/revel/revel"

func init() {
    // BEFORE: checkAuth runs before all actions in controllers.Users
    revel.InterceptMethod(controllers.Users.checkAuth, revel.BEFORE)

    // BEFORE: checkAuth runs before all actions in controllers.Products
    revel.InterceptMethod(controllers.Products.checkAuth, revel.BEFORE)

    // BEFORE: applies to all controllers (use a function, not a method)
    revel.InterceptFunc(checkLogin, revel.BEFORE, &controllers.Users{})

    // AFTER: logging after all actions
    revel.InterceptFunc(logRequest, revel.AFTER, revel.ALL_CONTROLLERS)

    // PANIC: handle panics
    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 called
    BI->>BI: Validate session / auth
    alt Interceptor returns a Result (non-nil)
        BI-->>Client: Redirect / Error (the action is NOT executed)
    else Interceptor returns nil
        BI->>Action: Continue to the action
        Action->>Action: Business logic
        Action->>AI: InterceptAFTER called
        AI->>AI: Logging, cleanup
        AI-->>Client: Response from the action
    end

Sessions and Flash Messages #

Revel provides HMAC-signed cookie-based sessions and flash messages for communication between redirects.

Sessions #

// Storing in the session
func (c Users) Login() revel.Result {
    // ... validate credentials
    c.Session["userID"]   = strconv.Itoa(user.ID)
    c.Session["userRole"] = user.Role
    c.Session.SetNoExpiration() // or SetDefaultExpiration()
    return c.Redirect(App.Index)
}

// Reading from the 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)
}

// Clearing the session (logout)
func (c Users) Logout() revel.Result {
    for k := range c.Session {
        delete(c.Session, k)
    }
    return c.Redirect(App.Index)
}

Flash Messages #

Flash messages are temporary data that only survive for one more request — ideal for success/error messages after a redirect:

// Storing flash messages
func (c Users) Create() revel.Result {
    // ...after saving successfully
    c.Flash.Success("User created successfully!")
    c.Flash.Error("Failed to send the confirmation email.")
    return c.Redirect(Users.List)
}

// In the template — flash is automatically available
<!-- The template reads the flash automatically from .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}}

Configuration #

Revel uses the conf/app.conf file with a key=value format that supports multiple environments (dev, test, prod):

# conf/app.conf

# Basic application configuration
app.name = MyApp
app.secret = replace-with-a-long-and-secure-random-string

# HTTP server
http.addr =
http.port = 9000
http.ssl  = false

# Active mode (dev, test, prod)
mode.dev = true

# ────────────────────────────────────────
# Per-environment overrides — prefix with the mode name
# ────────────────────────────────────────

# 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

Reading configuration in code:

// Reading configuration values
dbDriver, _ := revel.Config.String("db.driver")
dbSpec,   _ := revel.Config.String("db.spec")
port,     _ := revel.Config.Int("http.port")
isPretty, _ := revel.Config.Bool("results.pretty")

// With default values
timeout, _ := revel.Config.IntDefault("http.timeout", 30)
logLevel    := revel.Config.StringDefault("log.level", "info")

Job Scheduler #

Revel includes a cron-based job scheduler for running scheduled tasks — something you’d have to set up manually in other frameworks:

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() {
    // Delete expired sessions
    models.CleanExpiredSessions()
    revel.AppLog.Info("Session cleanup finished")
}

// app/init.go
func init() {
    // Run every day at 02:00
    jobs.Schedule("0 2 * * ?", CleanupJob{})

    // Run every 30 minutes
    jobs.Every(30*time.Minute, CleanupJob{})

    // Run once at application startup
    jobs.Now(SeedDataJob{})
}

Testing #

Revel provides an integrated testing framework 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 before each 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 after each test")
}

Run the tests:

revel test myapp dev

When Not to Use Revel #

Revel is a very opinionated framework. It’s important to understand its tradeoffs before choosing it:

Use Revel if:
  ✓ Building a web application with server-side rendering (SSR)
  ✓ The team wants strict MVC conventions in the Rails/Django style
  ✓ You need hot-reload, a job scheduler, and validation in one package
  ✓ Initial development speed matters more than architectural flexibility
  ✓ You don't need extreme raw performance

Don't use Revel if:
  ✗ Building a pure REST API consumed by an SPA frontend or mobile
     → Gin, Fiber, or Echo are far more suitable
  ✗ You need full flexibility in project structure
     → Revel enforces an immutable directory layout
  ✗ Performance is the top priority
     → MVC and template engine overhead adds latency
  ✗ The team plans to extensively use standard net/http libraries
     → Revel has its own separate ecosystem
  ✗ The project is a small microservice
     → Revel is too heavy for a service with only 3-5 endpoints
flowchart TD
    A{What are you building?} --> B["Web application\nwith HTML rendering"]
    A --> C["REST API /\nJSON service"]
    B --> D{Need strict\nMVC conventions?}
    D -- Yes --> E["✓ Revel"]
    D -- No --> F["Echo / Gin with\nmanual template engine"]
    C --> G{Top priority?}
    G -- Performance --> H["✓ Fiber"]
    G -- "net/http\necosystem" --> I["✓ Echo or Gin"]
    G -- "Familiar with\nExpress.js" --> H

Summary #

  • Full-stack MVC — Revel is the only Go framework that fully adopts MVC with an integrated CLI, hot-reload, template engine, validation, sessions, and job scheduler.
  • Declarative routing — routes are defined in conf/routes (not Go code); the whole routing table can be viewed at once, but there’s no compiler type checking.
  • Automatic parameter binding — Go method parameter names are matched directly to path params and query strings; Revel performs type conversion automatically.
  • Controllers as structs — any struct embedding revel.Controller can be a controller; public methods returning revel.Result automatically become actions.
  • Interceptors instead of middleware — use revel.InterceptMethod / revel.InterceptFunc in init.go for logic that runs before/after actions; more tied to controllers than middleware in other frameworks.
  • HMAC-signed session cookies — sessions are stored in cryptographically signed cookies; no server-side session store needed for common cases.
  • Flash messages — temporary data surviving one more request, very useful for success/error messages after redirects.
  • Not for pure REST APIs — if you’re building a JSON API without server-side rendering, Gin, Fiber, or Echo will be far more suitable and efficient.

← Previous: Echo   Next: Selenium →

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