Project Structure
master new generates a decoupled monorepo managed with npm workspaces: a MasterController API in backend/ and a Next.js app in frontend/. The backend comes production-shaped — layered configuration, a per-request database context, health endpoints, OpenAPI docs and an integration test — so there is nothing to bolt on later.
The tree#
my-app/
├── package.json # workspaces + dev/build/start/db scripts (master is a devDependency)
├── master.config.js # ports + frontend toggle (read by the CLI)
├── .env.example # NEXT_PUBLIC_API_URL, FRONTEND_URL, Section__Key examples
├── backend/
│ ├── package.json # mastercontroller ^2.11, masterrecord ^1.22, socket.io; "test": node --test
│ ├── server.js # boots the framework (config, scoped db, health, OpenAPI, auth, routes)
│ ├── app/
│ │ ├── routes.js # URL → controller#action (+ authorize / typed params / names)
│ │ ├── controllers/ # API controllers: this.db, this.model, this.ok/created/notFound…
│ │ │ └── healthController.js
│ │ ├── models/
│ │ │ ├── AppContext.js # registers entities (dbset) — one instance per request
│ │ │ ├── db.js # withDb(fn) for work outside a request (jobs, scripts)
│ │ │ ├── User.js # an example entity
│ │ │ └── db/migrations/ # generated migrations + context snapshot
│ │ └── sockets/ # WebSocket controllers
│ ├── middleware/ # auto-loaded pipeline (01-logger.js)
│ ├── config/
│ │ ├── appsettings.json # layered configuration defaults
│ │ ├── appsettings.production.json # production overrides (JSON logs, warn level)
│ │ └── environments/ # env.development.json, env.test.json, env.production.json
│ ├── db/ # SQLite files (db/ and db/test/)
│ └── test/
│ └── health.test.js # in-process integration test (master test)
└── frontend/
├── package.json # next, react
├── next.config.mjs
└── app/ # Next.js App Router
├── layout.tsx
├── page.tsx # welcome page — calls the backend /health
└── lib/api.ts # typed fetch helperKey files#
backend/server.js#
The equivalent of an ASP.NET Core Program.cs. In order, it:
- Defaults
NODE_ENVtodevelopment, setsmaster.root,master.environmentTypeandmaster.userSecretsId(the backend package name) so user secrets are read. - Calls
master.setupServer('http'), which builds the layeredmaster.configuration(appsettings.json→appsettings.<env>.json→env.<env>.json→ user secrets → env vars → CLI args). - Registers the database context per request:
master.addScoped('db', AppContext). Controllers usethis.db. - Configures CORS for the frontend origin (
FRONTEND_URLorFrontend:Url). - Adds enterprise middleware:
useResponseCompression(),useHealthChecks()with a database check,useOpenApi()(Swagger UI at/docsoutside production),useGracefulShutdown(). - Registers JWT bearer auth + an
AdminOnlypolicy only ifAuth:JwtSecret(orJWT_SECRET) is configured. - Auto-loads
middleware/*.jsalphabetically, thenstartMVC('app')(routes + controllers),start(server), optional sockets, andlisten(PORT).
process.env.NODE_ENV ??= 'development';
master.root = __dirname;
master.environmentType = process.env.NODE_ENV;
master.userSecretsId = pkg.name;
const server = master.setupServer('http');
const config = master.configuration;
master.addScoped('db', AppContext); // this.db in controllers (scoped DbContext)
master.useResponseCompression();
master.useHealthChecks({
path: '/health/report',
checks: { database: { check: () => master.useScope((scope) => scope.db.canConnect()), tags: ['ready'] } },
});
master.useOpenApi({ title: `${pkg.name} API`, version: pkg.version, ui: master.environment.isProduction ? false : '/docs' });
master.useGracefulShutdown();
const jwtSecret = config.get('Auth:JwtSecret') || process.env.JWT_SECRET;
if (jwtSecret) {
master.auth.addJwtBearer('bearer', { secret: jwtSecret, issuer: config.get('Auth:Issuer', pkg.name), expiresIn: config.get('Auth:ExpiresIn', '1h') });
master.auth.addPolicy('AdminOnly', { roles: ['admin'] });
}
await master.startMVC('app');
await master.start(server);
server.listen(PORT);backend/app/models/AppContext.js#
Your MasterRecord context. Generators keep the dbset list in sync.
import context from 'masterrecord/context';
import User from './User.js';
class AppContext extends context {
constructor() {
super();
this.env('config/environments'); // env.<NODE_ENV>.json, keyed by "AppContext"
this.dbset(User);
}
}
export default AppContext;backend/app/models/db.js#
There is no shared database singleton. Inside a request you use this.db — a fresh AppContext created for that request and disposed when the response closes (a shared context would mix the change sets of concurrent requests; see Change Tracking & Context Lifetime). For work outside a request — jobs, scripts, hosted services — db.js exports withDb(fn), which runs fn inside a DI scope:
import master from 'mastercontroller';
export function withDb(fn) {
return master.useScope((scope) => fn(scope.db));
}
export default withDb;
// usage from a script or hosted service:
// import { withDb } from './app/models/db.js';
// await withDb(async (db) => { const users = await db.User.toList(); });backend/app/controllers/healthController.js#
The tiny JSON endpoint the welcome page calls (GET /health). Load-balancer endpoints come from useHealthChecks: /health/live, /health/ready (runs the database check) and /health/report.
backend/config/#
appsettings.json + appsettings.production.json hold application settings; environments/env.<env>.json holds the database connection per environment. See Configuration.
backend/test/#
health.test.js boots the real pipeline in-process with master.createTestClient() and asserts on /health and /health/live. master test runs it with NODE_ENV=test. See Testing.
master.config.js#
Controls the dev/build/start orchestration — ports and whether a frontend exists.
export default {
frontend: true,
backendPort: 3001,
frontendPort: 3000,
backendDir: 'backend',
frontendDir: 'frontend',
};frontend/app/lib/api.ts#
A typed fetch helper that talks to the backend via NEXT_PUBLIC_API_URL.
--skip-frontend and you get just backend/ plus a master.config.js with frontend: false./health, /health/live, /health/ready, /health/report, /openapi.json and (outside production) the Swagger UI at /docs before you write a line of code.