Controllers
Controllers live in app/controllers/. Each request constructs a fresh instance and calls the action method named in the route. Actions receive the request object and reply with one of the response helpers.
Anatomy of a controller#
// server.js registered the context once: master.addScoped('db', AppContext)
export default class PostsController {
constructor(requestObject) {
this.requestObject = requestObject;
}
// GET /posts
async index() {
const data = await this.db.Post.toList(); // this.db = the request-scoped context
this.returnJson({ data });
}
// POST /posts
async create(obj) {
const post = this.db.Post.new();
Object.assign(post, obj.params.formData || {});
await post.save();
this.returnJson({ data: post });
}
// GET /posts/:id
async show(obj) {
const post = await this.db.Post.where((p) => p.id == $$, Number(obj.params.id)).single();
if (!post) return this.returnError(404, 'Not found');
this.returnJson({ data: post });
}
}master.addScoped('db', AppContext) gives every request its own MasterRecord context, built on first use and disposed when the response closes — never share one context across requests.Response helpers#
| Method | Effect |
|---|---|
this.returnJson(data) | Send JSON (200, or data.status if 4xx/5xx). |
this.returnError(code, msg, details?) | Send a structured JSON error. |
this.returnView(data) | Render the action’s view (needs a view engine). |
this.returnPartialView(view, data) | Render a partial without a layout. |
this.redirectTo(path) | Redirect (same-origin validated). |
this.redirectBack(fallback?) | Redirect to the referer, or a fallback. |
returnJson, returnError, returnView, and redirectTo — there is no this.json() or this.render().Accessing request data#
async show(obj) {
const id = obj.params.id; // route parameter (casing preserved)
const q = obj.params.query.search; // ?search=...
const body = obj.params.formData; // parsed JSON / form body
const method = obj.type; // 'get' | 'post' | ...
const req = obj.request; // raw Node request
const res = obj.response; // raw Node response
}application/json requests, the parsed body is available as obj.params.formData. Multipart uploads expose obj.params.formData.files.Per-request state#
Each instance gets this.state (shared with middleware) plus this.__request, this.__response, and the route context — set on the instance, never the prototype, for concurrency safety.
Typed results#
Since 2.4 controllers also have the ControllerBase-style helpers — this.ok(data), this.created(location, data), this.accepted(), this.noContent(), this.notFound(detail), this.conflict(), this.unprocessable(errors), this.problem({…}), this.file(pathOrBuffer, opts), this.statusCode(code, body). Errors are RFC 7807 application/problem+json.
async show(obj) {
const post = await this.db.Post.find(obj.params.id);
if (!post) return this.notFound(`Post ${obj.params.id} does not exist`);
this.ok(post);
}
async create() {
const post = await this.db.Post.add(this.model); // bound + validated by static schemas
await this.db.saveChanges();
this.created(`/posts/${post.id}`, post); // 201 + Location
}See Model Binding & Validation for the full list.
What else is on this#
| Member | What it is |
|---|---|
this.db, this.mailer, … | Every registered service; scoped ones are built lazily per request and disposed when the response closes. |
this.user | The ClaimsPrincipal (isAuthenticated, id, roles, isInRole()…) — Authentication. |
this.model, this.modelState | Bound, validated input from static schemas — Model Binding. |
this.logger | A category logger (category = controller class) already scoped with requestId — Logging. |
this.options, this.configuration, this.environment | Validated options, raw configuration, host environment — Configuration. |
this.urlFor(name, params) | URL generation for named routes. |
this.culture, this.localizer, this.apiVersion | Localization and API versioning context. |
this.signIn(), this.signOut(), this.authorize() | Auth actions — Authentication. |
Static declarations#
Cross-cutting behaviour is declared on the class, the way attributes are in ASP.NET:
export default class PostsController {
static authorize = { '*': true, index: false, show: false }; // [Authorize] per action
static allowAnonymous = ['health']; // [AllowAnonymous]
static schemas = { // [FromBody]/[FromQuery] + validation
create: { body: { title: 'string!', body: 'string' } },
index: { query: { page: { type: 'integer', default: 1, min: 1 } } },
};
static autoValidate = true; // false: inspect this.modelState yourself
static outputCache = { index: { duration: 60 } }; // [OutputCache]
static openapi = { create: { summary: 'Create a post' } }; // operation metadata
}