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
ApplicationControllerto share logic like auth across all controllers. -
Use
before_actionto load resources and DRY up your code. -
Prefix private helper methods with
_to prevent them from becoming routes.