Controllers & MVC Guide
Varel follows the Model-View-Controller (MVC) architectural pattern to organize your application code. This guide covers controllers, the MVC pattern, and best practices for structuring your application.
Table of Contents
- MVC Architecture
- Controllers Basics
- RESTful Controllers
- Code Organization
- Scaffolding
- Best Practices
- Advanced Patterns
MVC Architecture
What is MVC?
MVC separates your application into three interconnected components:
┌─────────────────────────────────────────────────┐
│ Request │
└────────────────────┬────────────────────────────┘
│
┌──────▼──────┐
│ Controller │ ← Handles request, coordinates
│ │ between Model and View
└──────┬───────┘
│
┌────────────┼────────────┐
│ │ │
┌────▼───┐ ┌───▼────┐ ┌──▼────┐
│ Model │ │ View │ │ Model │
│ │ │ │ │ │
│ User │ │ HTML │ │Product│
│ Post │ │ JSON │ │ Order │
└────────┘ └────────┘ └───────┘
│ │
└────────────┼────────────┘
│
Response
Model - Represents data and business logic
- Database interactions
- Data validation
- Business rules
- Data transformations
View - Presents data to the user
- HTML templates (VeeMarker)
- JSON responses
- XML, CSV, etc.
Controller - Coordinates between Model and View
- Receives requests
- Validates input
- Calls model methods
- Selects views to render
- Returns responses
Benefits of MVC
- Separation of Concerns - Each component has a single responsibility
- Testability - Easy to test each component independently
- Maintainability - Changes in one layer don't affect others
- Reusability - Models and views can be reused
- Parallel Development - Teams can work on different layers simultaneously
Controllers Basics
Note: Standard imports for controllers:
import leafscale.varel.http
import leafscale.varel.controller
import leafscale.varel.vareldb
Creating a Controller
Controllers are simple V structs with handler methods:
// controllers/products.v
module controllers
import leafscale.varel.http
import leafscale.varel.vareldb as db
import models
pub struct ProductsController {
db db.Database
}
// Create new controller instance
pub fn new_products_controller(database db.Database) ProductsController {
return ProductsController{
db: database
}
}
// Handler methods
pub fn (c ProductsController) index(mut ctx http.Context) http.Response {
products := models.Product.all(c.db) or {
return ctx.internal_error('Failed to load products')
}
return ctx.render('products/index', {
'products': products
})
}
pub fn (c ProductsController) show(mut ctx http.Context) http.Response {
id := ctx.param('id').int()
product := models.Product.find(c.db, id) or {
return ctx.not_found('Product not found')
}
return ctx.render('products/show', {
'product': product
})
}
Registering Routes
Register controller routes in your application:
// main.v
module main
import leafscale.varel.app as varel_app
import controllers
import leafscale.varel.vareldb as db
fn main() {
mut web_app := varel_app.new('My Shop')
// Connect to database
database := db.connect(db.Config{
host: 'localhost'
database: 'shop'
user: 'postgres'
})!
// Create controller
products_ctrl := controllers.new_products_controller(database)
// Register routes
web_app.get('/products', products_ctrl.index)!
web_app.get('/products/:id', products_ctrl.show)!
web_app.listen(':8080')
}
Controller Methods
Controllers are just collections of handler functions:
pub fn (c ProductsController) index(mut ctx http.Context) http.Response {
// List all products
}
pub fn (c ProductsController) show(mut ctx http.Context) http.Response {
// Show single product
}
pub fn (c ProductsController) new(mut ctx http.Context) http.Response {
// Show create form
}
pub fn (c ProductsController) create(mut ctx http.Context) http.Response {
// Create product from form data
}
pub fn (c ProductsController) edit(mut ctx http.Context) http.Response {
// Show edit form
}
pub fn (c ProductsController) update(mut ctx http.Context) http.Response {
// Update product from form data
}
pub fn (c ProductsController) destroy(mut ctx http.Context) http.Response {
// Delete product
}
RESTful Controllers
The Seven RESTful Actions
RESTful controllers follow a standard pattern:
| Action | HTTP Method | Path | Purpose |
|---|---|---|---|
| index | GET | /products | List all products |
| show | GET | /products/:id | Show single product |
| new | GET | /products/new | Show create form |
| create | POST | /products | Create product |
| edit | GET | /products/:id/edit | Show edit form |
| update | PUT/PATCH | /products/:id | Update product |
| destroy | DELETE | /products/:id | Delete product |
Complete RESTful Controller
// controllers/products.v
module controllers
import leafscale.varel.http
import leafscale.varel.vareldb as db
import models
pub struct ProductsController {
db db.Database
}
// GET /products - List all products
pub fn (c ProductsController) index(mut ctx http.Context) http.Response {
// Get pagination parameters
page := ctx.query('page') or { '1' }.int()
per_page := 20
// Fetch products with pagination
products := models.Product.paginate(c.db, page, per_page) or {
return ctx.internal_error('Failed to load products')
}
total := models.Product.count(c.db) or { 0 }
return ctx.render('products/index', {
'products': products
'page': page
'total_pages': (total + per_page - 1) / per_page
})
}
// GET /products/:id - Show single product
pub fn (c ProductsController) show(mut ctx http.Context) http.Response {
id := ctx.param('id').int()
product := models.Product.find(c.db, id) or {
return ctx.not_found('Product not found')
}
// Load related data
reviews := models.Review.for_product(c.db, id) or { [] }
return ctx.render('products/show', {
'product': product
'reviews': reviews
})
}
// GET /products/new - Show create form
pub fn (c ProductsController) new(mut ctx http.Context) http.Response {
// Load categories for dropdown
categories := models.Category.all(c.db) or { [] }
return ctx.render('products/new', {
'categories': categories
})
}
// POST /products - Create product
pub fn (c ProductsController) create(mut ctx http.Context) http.Response {
// Parse form data
name := ctx.form('name')
price_str := ctx.form('price')
description := ctx.form('description')
category_id_str := ctx.form('category_id')
// Validate
if name == '' {
return ctx.bad_request('Name is required')
}
price := price_str.f64()
if price <= 0 {
return ctx.bad_request('Price must be positive')
}
category_id := category_id_str.int()
// Create product
product := models.Product{
name: name
price: price
description: description
category_id: category_id
}
saved_product := models.Product.create(c.db, product) or {
return ctx.internal_error('Failed to create product: ${err}')
}
// Redirect to show page
return ctx.redirect('/products/${saved_product.id}')
}
// GET /products/:id/edit - Show edit form
pub fn (c ProductsController) edit(mut ctx http.Context) http.Response {
id := ctx.param('id').int()
product := models.Product.find(c.db, id) or {
return ctx.not_found('Product not found')
}
categories := models.Category.all(c.db) or { [] }
return ctx.render('products/edit', {
'product': product
'categories': categories
})
}
// PUT/PATCH /products/:id - Update product
pub fn (c ProductsController) update(mut ctx http.Context) http.Response {
id := ctx.param('id').int()
// Find existing product
mut product := models.Product.find(c.db, id) or {
return ctx.not_found('Product not found')
}
// Update fields
product.name = ctx.form('name')
product.price = ctx.form('price').f64()
product.description = ctx.form('description')
product.category_id = ctx.form('category_id').int()
// Validate
if product.name == '' {
return ctx.bad_request('Name is required')
}
// Save
models.Product.update(c.db, product) or {
return ctx.internal_error('Failed to update product: ${err}')
}
// Redirect to show page
return ctx.redirect('/products/${product.id}')
}
// DELETE /products/:id - Delete product
pub fn (c ProductsController) destroy(mut ctx http.Context) http.Response {
id := ctx.param('id').int()
models.Product.delete(c.db, id) or {
return ctx.internal_error('Failed to delete product: ${err}')
}
// Redirect to index
return ctx.redirect('/products')
}
API Controllers (JSON)
For JSON APIs, return JSON instead of rendering templates:
// controllers/api/products.v
module api
import leafscale.varel.http
import leafscale.varel.vareldb as db
import models
pub struct ProductsController {
db db.Database
}
// GET /api/products
pub fn (c ProductsController) index(mut ctx http.Context) http.Response {
products := models.Product.all(c.db) or {
return ctx.json_response(500, {
'error': 'Failed to load products'
})
}
return ctx.json_response(200, {
'products': products
})
}
// GET /api/products/:id
pub fn (c ProductsController) show(mut ctx http.Context) http.Response {
id := ctx.param('id').int()
product := models.Product.find(c.db, id) or {
return ctx.json_response(404, {
'error': 'Product not found'
})
}
return ctx.json_response(200, {
'product': product
})
}
// POST /api/products
pub fn (c ProductsController) create(mut ctx http.Context) http.Response {
// Parse JSON body
data := ctx.json_body<CreateProductRequest>() or {
return ctx.json_response(400, {
'error': 'Invalid JSON'
})
}
// Validate
if data.name == '' {
return ctx.json_response(400, {
'error': 'Name is required'
})
}
// Create
product := models.Product{
name: data.name
price: data.price
description: data.description
}
saved := models.Product.create(c.db, product) or {
return ctx.json_response(500, {
'error': 'Failed to create product'
})
}
return ctx.json_response(201, {
'product': saved
})
}
struct CreateProductRequest {
name string
price f64
description string
}
Code Organization
Directory Structure
Organize controllers by resource:
controllers/
├── home.v # HomeController
├── products.v # ProductsController
├── users.v # UsersController
├── sessions.v # SessionsController
└── api/
├── products.v # API::ProductsController
├── users.v # API::UsersController
└── v1/
└── products.v # API::V1::ProductsController
Namespace Controllers
Use V's module system for namespacing:
// controllers/api/products.v
module api
pub struct ProductsController {
// ...
}
// main.v
import controllers.api
fn main() {
mut web_app := varel_app.new('My App')
api_products := api.new_products_controller(db)
web_app.get('/api/products', api_products.index)!
web_app.listen(':8080')
}
Shared Controller Logic
Extract common logic into helper functions:
// controllers/base.v
module controllers
import leafscale.varel.http
import leafscale.varel.vareldb as db
// Helper to require authentication
pub fn require_auth(ctx http.Context) ?User {
user_id := ctx.session_get('user_id') or {
return none
}
user := models.User.find(db, user_id.int()) or {
return none
}
return user
}
// Helper to load and authorize resource
pub fn authorize_product(ctx http.Context, db db.Database) ?(models.Product, models.User) {
user := require_auth(ctx) or {
return none
}
id := ctx.param('id').int()
product := models.Product.find(db, id) or {
return none
}
if product.user_id != user.id {
return none
}
return product, user
}
// controllers/products.v
use base.{require_auth, authorize_product}
pub fn (c ProductsController) edit(mut ctx http.Context) http.Response {
product, user := authorize_product(ctx, c.db) or {
return ctx.unauthorized('Not authorized')
}
return ctx.render('products/edit', {
'product': product
'user': user
})
}
Scaffolding
Generate Complete Resource
Varel's CLI can generate a complete RESTful resource with one command:
# Basic scaffolding
varel generate scaffold Product name:string price:decimal description:text stock:int
# This generates everything you need for a complete CRUD resource!
What Gets Generated:
Controller (
controllers/products_controller.v)- All 7 RESTful actions (index, show, new, create, edit, update, destroy)
- Form validation
- Error handling
- Database integration
Model (
models/product.v)- Struct definition with all fields
- CRUD methods (create, find, all, update, delete)
- Database queries with proper SQL
Views (
views/products/)index.html- List all products (with table)show.html- Show single product detailsnew.html- Create form with validationedit.html- Edit form with current values
Database Migrations (
db/migrations/)YYYYMMDDHHMMSS_create_products.up.sql- Create tableYYYYMMDDHHMMSS_create_products.down.sql- Drop table (for rollback)
Tests (
tests/controllers/products_controller_test.v)- Tests for all seven actions
- Request/response validation
- Database transaction rollback
Supported Field Types
The scaffold generator supports these field types:
# String types
name:string # VARCHAR(255)
slug:string # VARCHAR(255)
# Text (unlimited length)
description:text # TEXT
content:text # TEXT
# Numbers
price:decimal # DECIMAL(10,2) for money
stock:int # INTEGER
quantity:int # INTEGER
rating:float # REAL
# Booleans
published:bool # BOOLEAN (true/false)
active:bool # BOOLEAN
# Dates and times
created_at:datetime # TIMESTAMP
updated_at:datetime # TIMESTAMP
published_on:date # DATE
# Complete example
varel generate scaffold Product \
name:string \
slug:string \
description:text \
price:decimal \
stock:int \
published:bool \
published_at:datetime
Run Migrations After Scaffolding
After generating a scaffold, run the migrations:
# Run the migration to create the products table
varel db migrate
# Output:
# Running migration: 20251013123045_create_products.up.sql
# ✓ Migration applied successfully
Add Routes to Your Application
Update main.v to register the generated controller:
// main.v
module main
import varel
import controllers
fn main() {
mut app := varel.new('My App')
// Database connection
db := // ... your database connection
// Create controller instance
mut products_ctrl := controllers.ProductsController{
db: db
}
// Register RESTful routes
app.get('/products', fn [mut products_ctrl] (mut ctx varel.Context) varel.Response {
return products_ctrl.index(mut ctx)
})!
app.get('/products/new', fn [mut products_ctrl] (mut ctx varel.Context) varel.Response {
return products_ctrl.new(mut ctx)
})!
app.post('/products', fn [mut products_ctrl] (mut ctx varel.Context) varel.Response {
return products_ctrl.create(mut ctx)
})!
app.get('/products/:id', fn [mut products_ctrl] (mut ctx varel.Context) varel.Response {
return products_ctrl.show(mut ctx)
})!
app.get('/products/:id/edit', fn [mut products_ctrl] (mut ctx varel.Context) varel.Response {
return products_ctrl.edit(mut ctx)
})!
app.put('/products/:id', fn [mut products_ctrl] (mut ctx varel.Context) varel.Response {
return products_ctrl.update(mut ctx)
})!
app.delete('/products/:id', fn [mut products_ctrl] (mut ctx varel.Context) varel.Response {
return products_ctrl.destroy(mut ctx)
})!
app.listen(':8080')
}
Now visit:
http://localhost:8080/products- List all productshttp://localhost:8080/products/new- Create new producthttp://localhost:8080/products/1- Show product #1http://localhost:8080/products/1/edit- Edit product #1
Customize Scaffolded Code
Scaffolding provides a starting point. Customize as needed:
// controllers/products.v (after scaffolding)
// Add custom action
pub fn (c ProductsController) featured(mut ctx http.Context) http.Response {
products := models.Product.where(c.db, 'featured = true') or {
return ctx.internal_error('Failed to load featured products')
}
return ctx.render('products/featured', {
'products': products
})
}
// Add authorization
pub fn (c ProductsController) edit(mut ctx http.Context) http.Response {
// Require authentication
user := require_auth(ctx) or {
return ctx.redirect('/login')
}
id := ctx.param('id').int()
product := models.Product.find(c.db, id) or {
return ctx.not_found('Product not found')
}
// Check ownership
if product.user_id != user.id && !user.is_admin {
return ctx.forbidden('Not authorized')
}
return ctx.render('products/edit', {
'product': product
})
}
Best Practices
1. Keep Controllers Thin
Move business logic to models:
// ❌ BAD - Business logic in controller
pub fn (c OrdersController) create(mut ctx http.Context) http.Response {
items := parse_cart_items(ctx)
mut total := 0.0
for item in items {
product := models.Product.find(c.db, item.product_id)!
if product.stock < item.quantity {
return ctx.bad_request('Insufficient stock')
}
total += product.price * item.quantity
}
// Apply discount
if total > 100 {
total *= 0.9
}
// Create order...
}
// ✅ GOOD - Business logic in model
pub fn (c OrdersController) create(mut ctx http.Context) http.Response {
items := parse_cart_items(ctx)
order := models.Order.create_from_cart(c.db, user_id, items) or {
return ctx.bad_request(err.msg())
}
return ctx.redirect('/orders/${order.id}')
}
2. Use Dependency Injection
Pass dependencies through constructor:
// ✅ GOOD
pub struct ProductsController {
db db.Database
cache cache.Cache
mailer mailer.Mailer
}
pub fn new_products_controller(db db.Database, cache cache.Cache, mailer mailer.Mailer) ProductsController {
return ProductsController{
db: db
cache: cache
mailer: mailer
}
}
3. Validate Input
Always validate user input:
pub fn (c ProductsController) create(mut ctx http.Context) http.Response {
name := ctx.form('name')
price_str := ctx.form('price')
// Validate required fields
if name == '' {
return ctx.bad_request('Name is required')
}
// Validate types
price := price_str.f64()
if price <= 0 {
return ctx.bad_request('Price must be positive')
}
// Validate business rules
if name.len < 3 {
return ctx.bad_request('Name must be at least 3 characters')
}
if price > 10000 {
return ctx.bad_request('Price exceeds maximum')
}
// Create product...
}
4. Handle Errors Gracefully
Return appropriate HTTP status codes:
pub fn (c ProductsController) show(mut ctx http.Context) http.Response {
id := ctx.param('id').int()
if id <= 0 {
return ctx.bad_request('Invalid product ID')
}
product := models.Product.find(c.db, id) or {
// Log error
eprintln('Failed to find product ${id}: ${err}')
// Return user-friendly error
return ctx.not_found('Product not found')
}
return ctx.render('products/show', {
'product': product
})
}
5. Use Resource Routes
Group related routes:
fn main() {
mut web_app := varel_app.new('My App')
products := web_app.group('/products')
products.get('/', products_ctrl.index)!
products.get('/new', products_ctrl.new)!
products.post('/', products_ctrl.create)!
products.get('/:id', products_ctrl.show)!
products.get('/:id/edit', products_ctrl.edit)!
products.put('/:id', products_ctrl.update)!
products.delete('/:id', products_ctrl.destroy)!
web_app.listen(':8080')
}
Advanced Patterns
Before/After Filters
Use middleware for before/after actions:
// controllers/admin.v
pub fn (c AdminController) setup_routes(mut router router.Router) {
admin := router.group('/admin')
// Before filter - require admin
admin.use(middleware.require_admin())
// After filter - log admin actions
admin.use(middleware.audit_log())
admin.get('/dashboard', c.dashboard)!
admin.get('/users', c.users)!
}
Controller Concerns
Share behavior across controllers:
// controllers/concerns/pagination.v
module concerns
pub fn paginate(ctx http.Context) (int, int) {
page := ctx.query('page') or { '1' }.int()
per_page := ctx.query('per_page') or { '20' }.int()
// Validate
if page < 1 {
page = 1
}
if per_page < 1 || per_page > 100 {
per_page = 20
}
offset := (page - 1) * per_page
return offset, per_page
}
// controllers/products.v
import controllers.concerns
pub fn (c ProductsController) index(mut ctx http.Context) http.Response {
offset, limit := concerns.paginate(ctx)
products := models.Product.limit(c.db, offset, limit) or {
return ctx.internal_error('Failed to load products')
}
return ctx.render('products/index', {
'products': products
})
}
Nested Resources
Handle nested RESTful resources:
// controllers/reviews.v - /products/:product_id/reviews
pub struct ReviewsController {
db db.Database
}
// GET /products/:product_id/reviews
pub fn (c ReviewsController) index(mut ctx http.Context) http.Response {
product_id := ctx.param('product_id').int()
product := models.Product.find(c.db, product_id) or {
return ctx.not_found('Product not found')
}
reviews := models.Review.for_product(c.db, product_id) or {
return ctx.internal_error('Failed to load reviews')
}
return ctx.render('reviews/index', {
'product': product
'reviews': reviews
})
}
// POST /products/:product_id/reviews
pub fn (c ReviewsController) create(mut ctx http.Context) http.Response {
product_id := ctx.param('product_id').int()
rating := ctx.form('rating').int()
comment := ctx.form('comment')
review := models.Review{
product_id: product_id
rating: rating
comment: comment
}
saved := models.Review.create(c.db, review) or {
return ctx.internal_error('Failed to create review')
}
return ctx.redirect('/products/${product_id}/reviews')
}
// main.v
fn main() {
mut web_app := varel_app.new('My App')
// Nested resource routes
reviews := web_app.group('/products/:product_id/reviews')
reviews.get('/', reviews_ctrl.index)!
reviews.post('/', reviews_ctrl.create)!
web_app.listen(':8080')
}
Summary
You've learned:
✅ MVC architecture and its benefits ✅ Creating controllers with handler methods ✅ The seven RESTful actions (index, show, new, create, edit, update, destroy) ✅ JSON API controllers ✅ Code organization and namespacing ✅ Scaffolding complete resources ✅ Best practices (thin controllers, validation, error handling) ✅ Advanced patterns (filters, concerns, nested resources)
Continue to the Database Guide to learn about working with PostgreSQL!