ESC
Type to search...
S
Soli Docs

Controllers

Controllers are the heart of your application. They handle HTTP requests, process data, and determine the response. Soli supports both modern class-based controllers and simple function-based handlers.

Class-Based Controllers

Recommended for most applications. They provide structure, inheritance, and powerful hooks like before_action.

class PostsController < ApplicationController
    static
        this.layout = "layouts/posts"
    end

    def index(req)
        let posts = Post.all()
        render("posts/index", {
            "title": "All Posts",
            "posts": posts
        })
    end

    def show(req)
        let id = req.params["id"]
        let post = Post.find(id)
        if post == null
            return error(404, "Post not found")
        end

        render("posts/show", {
            "title": post["title"],
            "post": post
        })
    end
end

Lifecycle Hooks

Execute code before or after actions. Perfect for authentication or data loading.

this.layout

Set the layout template for all actions

this.before_action

Run code before action executes

this.after_action

Run code after action executes

class PostsController < ApplicationController
    static
        # Set layout for all actions
        this.layout = "layouts/posts"

        # Run before ALL actions
        this.before_action = def(req)
            print("Processing: " + req.path)
            req
        end

        # Run before SPECIFIC actions only
        this.before_action(:show, :edit) = def(req)
            let post = Post.find(req.params["id"])
            if post == null then return error(404, "Not Found") end
            req["post"] = post
            req
        end

        # Run after SPECIFIC actions
        this.after_action(:create, :update) = def(req, response)
            # Log activity after create/update
            print("Action completed")
            response
        end
    end

    # ... actions ...
end

Controller Inheritance

Controllers support multi-level inheritance. Create a base controller to share logic across multiple controllers. Hooks like before_action, after_action, and layout are automatically inherited by child controllers.

class BaseController extends Controller
    static
        this.layout = "application"

        this.before_action = def(req)
            let user_id = req.session["user_id"]
            if user_id != null
                req["current_user"] = User.find(user_id)
            end
            req
        end
    end

    # Shared helper available to all child controllers
    def _current_user
        this.req["current_user"]
    end
end
# Inherits layout, before_action, and _current_user from BaseController
class PostsController extends BaseController
    static
        # Add controller-specific hook (runs after parent's before_action)
        this.before_action(:show, :edit) = def(req)
            let post = Post.find(req.params["id"])
            if post == null then return error(404, "Not Found") end
            req["post"] = post
            req
        end
    end

    def index(req)
        render("posts/index", { "posts": Post.all })
    end

    def show(req)
        render("posts/show", { "post": req["post"] })
    end
end
# Even deeper inheritance works
class AdminController extends BaseController
    static
        this.layout = "admin"  # Override parent layout

        this.before_action = def(req)
            if req["current_user"] == null
                return redirect("/login")
            end
            req
        end
    end
end

Inheritance Rules

  • Methods are inherited and can be overridden. Use super.method() to call the parent version.
  • before_action / after_action hooks are inherited from parent controllers. Child hooks run after parent hooks.
  • layout is inherited if the child doesn't set its own.
  • Fields declared in parent classes are available in child instances.

Function-Based Controllers

Great for simple APIs or microservices where you don't need the full power of classes.

# GET /health
def health(req)
    {
        "status": 200,
        "headers": {"Content-Type": "application/json"},
        "body": "{\"status\":\"ok\"}"
    }
end

The Request Object

Access all HTTP request details through the req parameter.

req.params

Route parameters & query strings

req.body

Raw request body content

req.headers

HTTP headers map

req.session

User session data storage

Response Types

render(template, data?)

Render an HTML view template with optional data.

render("home/index", { "title": "Welcome" })
render_json(data, status?)

Render a JSON response with automatic Content-Type: application/json header. Data is automatically serialized to JSON.

render_json({ "users": users })
render_json({ "error": "Not found" }, 404)
render_text(text, status?)

Render a plain text response with automatic Content-Type: text/plain header.

render_text("pong")
render_text("Not allowed", 403)
redirect(url)

Create a redirect response (302 Found) to the specified URL.

redirect("/login")
redirect("/posts/" + post["id"])
error(status, message)

Return an error response with the given HTTP status code and message.

error(404, "Page Not Found")
error(403, "Forbidden")

Pro Tip: Keep Controllers Thin

Delegate complex business logic to Models or Service objects. Controllers should primarily focus on handling the HTTP request and selecting the correct response.

Best Practices

  • Use class-based controllers for better organization and reuse.
  • Create a base ApplicationController to share logic like auth across all controllers.
  • Use before_action to load resources and DRY up your code.
  • Prefix private helper methods with _ to prevent them from becoming routes.

Next Steps