Controllers, Requests, Validation & Views
Controllers organize request-handling logic in a central location. They receive an HTTP request, perform actions, and return an HTTP response.
Controllers
Controllers inherit from the base class craft.http.Controller. Action methods accept a Request parameter (plain def is the norm — the ORM is synchronous; async def actions are also awaited by the kernel if you need them for other I/O):
from craft.http import Controller, Request
from app.Models.Post import Post
class PostController(Controller):
def index(self, request: Request):
posts = Post.query().get()
return self.view("posts.index", {"posts": posts})
HTTP Requests
The Request object exposes incoming payload data, files, and server properties:
# Retrieve request input parameters (combines JSON body, Form data, and Query arguments)
title = request.input("title", default="Default Title")
# Retrieve all input parameters as a dictionary
data = request.all()
# Access request properties
method = request.method
path = request.path
headers = request.headers
cookies = request.cookies
Form Validation (FormRequest)
For complex validation layouts, define a FormRequest class mapping a declarative rules list to incoming request data. The validator enforces constraints dynamically:
Example FormRequest
from craft.validation import FormRequest
class StorePostRequest(FormRequest):
def authorize(self) -> bool:
# Perform authorization checks (e.g. return Auth.check())
return True
def rules(self) -> dict:
return {
"title": ["required", "string", "max:255"],
"body": ["required", "string"],
"published": ["sometimes", "boolean"],
}
Validating Requests in Controllers
Invoke .validated() inside your controller action. It authorizes first (raising AuthorizationException, rendered as 403), then validates — a failure raises ValidationException, rendered by the exception handler as a 422 Unprocessable Entity response. On success it returns only the fields you wrote rules for:
from craft.support import redirect
def store(self, request: Request):
# Authorize and validate the request
validated_data = StorePostRequest(request).validated()
# Create post using validated attributes
post = Post.create(validated_data)
return redirect(route="posts.show", id=post.get_attribute("id"))
redirect is a plain function: redirect(url) for a literal URL, or redirect(route="name", **params) for a named route. Inside a controller, self.redirect(...) does the same.
Views & Templating (Forge Engine)
The Forge templating engine compiles HTML files inside resources/views/. It compiles Forge markup syntax into pure Jinja2 templates.
Rendering
Render templates with the controller's self.view() helper:
return self.view("posts.index", {"posts": posts})
Prefer self.view(): rendering errors propagate to the exception handler, so a missing template raises TemplateNotFound instead of failing silently. (A standalone view() function also exists in craft.support, but it swallows rendering errors and returns a placeholder response — avoid it in application code. See Views.)
Syntax Examples
Layout Extension (resources/views/posts/index.forge.py)
@extends("layouts.app")
@section("title", "Active Posts")
@section("content")
<h1 class="text-3xl font-bold">Latest Posts</h1>
<div class="space-y-4">
@foreach(posts as post)
<article class="p-4 border rounded">
<h2>{{ post.title }}</h2>
<p>{{ post.body }}</p>
</article>
@endforeach
</div>
@endsection
Authorization & CSRF Helpers
<!-- Display blocks to authenticated users only -->
@auth
<p>Welcome, {{ auth().get_attribute('name') }}</p>
@endauth
<!-- Form setup with CSRF protection token -->
<form action="/posts" method="POST">
@csrf
<input type="text" name="title" />
<button type="submit">Submit</button>
</form>