Getting Started
This guide takes you from an empty folder to a running full-stack application: a Next.js frontend, a MasterController API, and a MasterRecord database — all wired together.
1. Install the CLI#
$ npm install -g master
Master requires Node.js 22.12 or newer. Verify the install:
$ master --version$ master info
2. Create a new app#
$ master new my-app
This scaffolds the monorepo, installs dependencies, and initializes git. Choose your database with --db (defaults to SQLite — zero setup):
$ master new my-app --db postgres$ master new my-app --db mysql$ master new blog --skip-frontend # API only
3. Set up the database#
$ cd my-app$ master db migrate
For SQLite this creates backend/db/appcontext.sqlite instantly (and a separate db/test/ database is used by master test). For MySQL/Postgres, edit the credentials in backend/config/environments/env.development.json first (keyed by the context name, AppContext):
{
"AppContext": {
"type": "postgres",
"host": "127.0.0.1",
"port": 5432,
"database": "my_app_development",
"user": "postgres",
"password": ""
}
}4. Run it#
$ master dev
- API → http://localhost:3001 — OpenAPI at
/openapi.json, Swagger UI at /docs, health at/health,/health/live,/health/ready - Web → http://localhost:3000
Both restart on change, and each process’s output is prefixed ([backend], [frontend]). The homepage calls the backend’s /health endpoint to prove the full stack is connected.
5. Build a feature#
Scaffold a complete resource — a model, a validated RESTful API controller, typed routes, and a Next.js page — with one command. master db new needs no enable step; migrations are switched on the first time you use it:
$ master generate scaffold post title:string body:text published:bool$ master db new AddPosts$ master db migrate
You now have a working /posts JSON API and a /posts page. Try it:
curl -X POST http://localhost:3001/posts \
-H 'Content-Type: application/json' \
-d '{"title":"Hello","body":"By the power of Grayskull","published":true}'
# 201 Created + Location: /posts/1
curl -X POST http://localhost:3001/posts -H 'Content-Type: application/json' -d '{}'
# 400 ValidationProblemDetails: {"errors":{"title":["title is required"], ...}}
curl http://localhost:3001/posts/abc
# 404 — the route is /posts/:id(int), so non-integers never reach the controller
curl http://localhost:3001/postsThe generated controller shows the whole ASP.NET-style toolkit in about forty lines:
// PostsController — RESTful JSON API backed by MasterRecord.
// this.db is a request-scoped AppContext (ASP.NET/EF: scoped DbContext) — registered in server.js.
// Input is bound + validated from `static schemas` into this.model (invalid -> 400 ValidationProblemDetails).
export default class PostsController {
static schemas = {
create: { body: { title: { type: 'string', required: true }, body: { type: 'string', required: true }, published: { type: 'boolean' } } },
update: { body: { title: { type: 'string' }, body: { type: 'string' }, published: { type: 'boolean' } } },
};
// Protect writes once auth is configured in server.js (ASP.NET [Authorize]):
// static authorize = { create: true, update: true, destroy: true };
constructor(requestObject) {
this.requestObject = requestObject;
}
// GET /posts
async index() {
const data = await this.db.Post.toList();
this.ok({ data });
}
// GET /posts/:id
async show(obj) {
const item = await this.db.Post.find(obj.params.id);
if (!item) return this.notFound('Post not found');
this.ok({ data: item });
}
// POST /posts
async create() {
const item = this.db.Post.new();
Object.assign(item, this.model);
await this.db.saveChanges();
this.created(`/posts/${item.id}`, { data: item });
}
// PUT /posts/:id
async update(obj) {
const item = await this.db.Post.find(obj.params.id);
if (!item) return this.notFound('Post not found');
Object.assign(item, this.model);
await this.db.saveChanges();
this.ok({ data: item });
}
// DELETE /posts/:id
async destroy(obj) {
const item = await this.db.Post.find(obj.params.id);
if (!item) return this.notFound('Post not found');
this.db.Post.remove(item);
await this.db.saveChanges();
this.noContent();
}
}Open http://localhost:3001/docs — the new endpoints, their parameters and request schemas are already in the Swagger UI.
6. Configuration, secrets and auth#
The backend reads configuration ASP.NET-style — config/appsettings.json → appsettings.<env>.json → env.<env>.json → user secrets → environment variables (Section__Key). Keep secrets out of the repo:
$ master secrets set Auth:JwtSecret "$(openssl rand -hex 32)" # stored in ~/.master, not in git$ master secrets list # masked; --reveal to show
Once Auth:JwtSecret is present, server.js registers a JWT bearer scheme and an AdminOnly policy. Uncomment static authorize in the controller (or add { authorize: true } to a route) and writes require a signed-in user. In production set Auth__JwtSecret as an environment variable. See Secrets and Configuration.
7. Test#
$ master test
Runs backend/test/*.test.js with Node’s built-in runner under NODE_ENV=test. The scaffolded health.test.js boots the real pipeline in-process with master.createTestClient() — no ports, no mocks. See Testing.
server.js boots with setupServer() → addScoped('db', AppContext) → middleware → startMVC() → start(). Controllers get this.db (a fresh context per request), this.model (validated input), this.user, this.logger, and reply with this.ok / created / noContent / notFound / problem. Models register in AppContext.js via this.dbset(Entity). The frontend calls the API through frontend/app/lib/api.ts.