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

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

  1. Separation of Concerns - Each component has a single responsibility
  2. Testability - Easy to test each component independently
  3. Maintainability - Changes in one layer don't affect others
  4. Reusability - Models and views can be reused
  5. 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:

  1. Controller (controllers/products_controller.v)

    • All 7 RESTful actions (index, show, new, create, edit, update, destroy)
    • Form validation
    • Error handling
    • Database integration
  2. Model (models/product.v)

    • Struct definition with all fields
    • CRUD methods (create, find, all, update, delete)
    • Database queries with proper SQL
  3. Views (views/products/)

    • index.html - List all products (with table)
    • show.html - Show single product details
    • new.html - Create form with validation
    • edit.html - Edit form with current values
  4. Database Migrations (db/migrations/)

    • YYYYMMDDHHMMSS_create_products.up.sql - Create table
    • YYYYMMDDHHMMSS_create_products.down.sql - Drop table (for rollback)
  5. 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 products
  • http://localhost:8080/products/new - Create new product
  • http://localhost:8080/products/1 - Show product #1
  • http://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!