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 likeairornodemonfor 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 --> GHow 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 --> KRevel MVC Components vs Go Standard #
| MVC Component | Revel Implementation | Standard Go (net/http) Equivalent |
|---|---|---|
| Model | Ordinary Go structs under the app/models/ subfolder | Go structs, DB entities, or DTOs |
| View | HTML templates (html/template) in the app/views/ folder | html/template.ParseFiles / manual HTML writing |
| Controller | Go structs embedding *revel.Controller in app/controllers/ | HTTP Handler functions (http.HandlerFunc) |
| Routing | A separate declarative text file at conf/routes | Multiplexer/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
endSessions 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" --> HSummary #
- 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.Controllercan be a controller; public methods returningrevel.Resultautomatically become actions.- Interceptors instead of middleware — use
revel.InterceptMethod/revel.InterceptFuncininit.gofor 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.