MasterController

Controllers

Plain ESM classes that turn requests into responses.

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#

postsController.js
// 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 });
  }
}
Where does this.db come from?
From dependency injection: 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#

MethodEffect
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.
It's returnJson, not json
The response methods are returnJson, returnError, returnView, and redirectTo — there is no this.json() or this.render().

Accessing request data#

request-data.js
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
}
Note
For 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.

typed results
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#

MemberWhat it is
this.db, this.mailer, …Every registered service; scoped ones are built lazily per request and disposed when the response closes.
this.userThe ClaimsPrincipal (isAuthenticated, id, roles, isInRole()…) — Authentication.
this.model, this.modelStateBound, validated input from static schemasModel Binding.
this.loggerA category logger (category = controller class) already scoped with requestIdLogging.
this.options, this.configuration, this.environmentValidated options, raw configuration, host environment — Configuration.
this.urlFor(name, params)URL generation for named routes.
this.culture, this.localizer, this.apiVersionLocalization 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:

postsController.js
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
}