Master Guides

Architecture

How the pieces fit — and how Next.js gives your app a face.

A Master app is a decoupled full-stack monorepo. The browser talks to a Next.js frontend; the frontend talks to a MasterController API; the API talks to your database through the MasterRecord ORM. The Master CLI scaffolds all of it and runs the whole stack with one command.

The three Masters united

The stack, top to bottom#

Frontend
Next.js — the face of your app

The App Router UI users actually see and touch. Server Components fetch data, Client Components add interactivity. This is where everything comes together visually.

▼   HTTP / JSON   ▼
Backend
MasterController — the API

A fast ESM MVC server with ASP.NET Core-style plumbing. Routes map URLs to controllers; controllers bind and validate input, use a request-scoped database context, and reply with typed JSON results. Auth, DI scopes, layered configuration, logging, health checks, OpenAPI, caching, compression and real-time sockets are built in.

▼   queries   ▼
Data
MasterRecord — the ORM

Code-first models and an expressive query language over SQLite, MySQL, or PostgreSQL. Migrations keep the schema in step with your models.

Next.js: how the front end ties it all together#

In a Master app, Next.js is the entire front end — not a sprinkle of client widgets, but the real, full App Router framework. It is the one place your users meet your application, and it pulls the whole stack into a single, coherent experience.

Server Components fetch from your API#

Most pages are React Server Components. They run on the server, call your MasterController API directly (no CORS, no client round-trip), and stream finished HTML to the browser — fast first paint, SEO-friendly, no loading spinners for the initial view.

frontend/app/posts/page.tsx (Server Component)
import { api } from '../lib/api';

export default async function PostsPage() {
  // Runs on the server — talks straight to MasterController.
  const { data } = await api<{ data: Post[] }>('/posts');
  return <ul>{data.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}

Client Components add interactivity#

Anything interactive — forms, live updates, optimistic UI — is a 'use client' component that calls the same API from the browser. One typed helper, app/lib/api.ts, is the single doorway between the front end and the back end.

a Client Component
'use client';
import { api } from '../lib/api';

async function createPost(title: string) {
  return api('/posts', { method: 'POST', body: JSON.stringify({ title }) });
}

One helper, one contract#

Because every call flows through api(), the boundary between frontend and backend is explicit and typed. Change the API base URL once (via NEXT_PUBLIC_API_URL) and the whole front end follows — local, staging, or production.

Why this combination is powerful
You get Next.js’s world-class frontend — Server Components, streaming, image optimization, file-based routing — and a real backend framework with an ORM behind it. No cramming business logic into route handlers, no hand-rolled data layer. The front end stays a front end; the back end stays a back end.

Inside the backend#

backend/server.js is the composition root — the same job an ASP.NET Core Program.cs does. It builds configuration, registers services, adds middleware, loads routes and controllers, and listens:

boot sequence
NODE_ENV (default development) + master.root + master.userSecretsId
  → master.setupServer('http')          configuration: appsettings.json → appsettings.<env>.json
                                         → env.<env>.json → user secrets → env vars → CLI args
  → master.addScoped('db', AppContext)  one MasterRecord context per request  (this.db)
  → cors · useResponseCompression · useHealthChecks · useOpenApi · useGracefulShutdown
JWT bearer auth + policies          (only when Auth:JwtSecret is configured)
  → middleware/*.js                     auto-loaded, alphabetical
  → master.startMVC('app')              app/routes.js + app/controllers
  → master.start(server)                finalize pipeline, validate options, start hosted services
  → server.listen(PORT)

A request-scoped database context#

There is no database singleton. master.addScoped('db', AppContext) gives every request its own AppContext — created lazily on first use, disposed when the response closes — exactly like a scoped EF DbContext. Controllers reach it as this.db; work outside a request (jobs, scripts) uses withDb(fn) from app/models/db.js, which opens a DI scope. Because MasterRecord’s change tracking is a unit of work, this is what keeps concurrent requests from committing each other’s changes.

Controllers: bind, validate, reply#

backend/app/controllers/postsController.js (scaffolded)
export default class PostsController {
  static schemas = {                                   // model binding + validation -> this.model
    create: { body: { title: { type: 'string', required: true }, body: { type: 'string', required: true } } },
    update: { body: { title: { type: 'string' }, body: { type: 'string' } } },
  };
  // static authorize = { create: true, update: true, destroy: true };   // [Authorize] once auth is on

  constructor(requestObject) { this.requestObject = requestObject; }

  async index() {
    const data = await this.db.Post.toList();          // this.db = this request's AppContext
    this.ok({ data });
  }

  async create() {
    const item = this.db.Post.new();
    Object.assign(item, this.model);                   // already validated (else 400 ValidationProblemDetails)
    await this.db.saveChanges();
    this.created(`/posts/${item.id}`, { data: item });
  }
}

Operational endpoints, for free#

  • /health — the welcome page’s JSON ping; /health/live, /health/ready (runs the database check) and /health/report for load balancers and orchestrators.
  • /openapi.json — an OpenAPI 3.1 document generated from routes, typed params, static schemas and auth; Swagger UI at /docs outside production.
  • Responses are gzip/brotli compressed; SIGTERM drains in-flight requests before exit.

The life of a request#

  1. A user opens /posts in the browser → Next.js renders the page.
  2. The Server Component calls api('/posts') → an HTTP request to MasterController.
  3. The pipeline runs (logging, compression, auth if configured); the router matches GET /postsposts#index, checks typed params and authorize, and binds/validates input from static schemas.
  4. A fresh AppContext is created for the request; the controller runs this.db.Post.toList().
  5. MasterRecord builds and runs the SQL, returns tracked model instances.
  6. The controller replies with this.ok(...); the scope (and its context) is disposed; Next.js renders the HTML and streams it back.

Why decoupled?#

  • Independent scaling & deployment — host the Next.js front end on the edge and the API on a Node host; scale each to its own load.
  • Clean separation of concerns — UI code never touches SQL; API code never renders HTML.
  • Reusable API — the same MasterController API can serve a mobile app or third party, not just your website.
  • One dev experience — despite being two services, master dev runs them together with shared config and hot reload.