Controllers
SemitraController is the request coordinator.
It owns orchestration, not persistence rules or response schemas.
What controllers get
Section titled “What controllers get”Every controller instance has access to:
requestparamsenvctxrouteruntimecurrentTenantlocals
Application controllers can also hydrate currentUser.
Example application controller
Section titled “Example application controller”The example app centralizes current-user hydration once:
import { SemitraController } from "@semitra/cli";
export default class ApplicationController extends SemitraController { static { this.beforeAction("hydrateCurrentUser"); }
hydrateCurrentUser(): void { const role = this.request.headers.get("x-semitra-role"); if (!role) { this.setCurrentUser(null); return; }
const id = this.request.headers.get("x-semitra-user-id") ?? "edge-user"; this.setCurrentUser({ id, role }); }}Common controller responsibilities
Section titled “Common controller responsibilities”- parse and validate input
- load records
- authorize actions
- scope collections
- dispatch jobs or events
- render responses
Example action
Section titled “Example action”The example app’s posts controller shows the full Semitra controller pattern:
import { s } from "@semitra/cli";import ApplicationController from "../../application_controller.ts";import Post from "../../../models/post.ts";import PostResource from "../../../resources/post_resource.ts";
const CreatePostParams = s.object({ post: s.object({ title: s.string().min(3), content: s.string().optional() })});
export default class PostsController extends ApplicationController { static { this.useResource(PostResource, { only: ["create"] }); this.verifyAuthorized({ only: ["create"] }); }
async create() { const input = this.params.require(CreatePostParams); const Posts = this.model(Post);
await this.authorize(Post, "create"); const post = Posts.build(input.post);
if (!(await post.save())) { return this.renderErrors(post.errors); }
return this.renderResource(post, { status: 201 }); }}That is the intended split of responsibilities: the controller validates, authorizes, builds the record through the request-bound model class, and then renders the resource. It does not inline persistence rules or serialization.
Rendering
Section titled “Rendering”Controllers can render:
renderJson(data)renderResource(record)renderResourceCollection(records)renderErrors(errors)renderText(body)renderCsv(body)renderBinary(body)head(status)
If a controller action returns undefined, Semitra treats it as an empty
204 response.
Request-scoped model access
Section titled “Request-scoped model access”Controllers use this.model(Model) instead of reaching into the model class raw.
That binds the record layer to:
- resolved adapters
- current tenant
- tenant database provider
- request runtime context
Guardrails
Section titled “Guardrails”Semitra includes controller guardrails that enforce policy discipline:
verifyAuthorized()verifyPolicyScoped()
These are useful when you want action methods to fail fast if a required authorization step was skipped.