Master Guides

Getting Started

Build and run your first full-stack Master app in a few minutes.

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#

terminal
$ npm install -g master

Master requires Node.js 22.12 or newer. Verify the install:

terminal
$ master --version
$ master info

2. Create a new app#

terminal
$ master new my-app

This scaffolds the monorepo, installs dependencies, and initializes git. Choose your database with --db (defaults to SQLite — zero setup):

terminal
$ master new my-app --db postgres
$ master new my-app --db mysql
$ master new blog --skip-frontend # API only

3. Set up the database#

terminal
$ 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):

env.development.json
{
  "AppContext": {
    "type": "postgres",
    "host": "127.0.0.1",
    "port": 5432,
    "database": "my_app_development",
    "user": "postgres",
    "password": ""
  }
}

4. Run it#

terminal
$ master dev

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:

terminal
$ 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:

terminal
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/posts

The generated controller shows the whole ASP.NET-style toolkit in about forty lines:

backend/app/controllers/postsController.js (generated)
// 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>.jsonenv.<env>.json → user secrets → environment variables (Section__Key). Keep secrets out of the repo:

terminal
$ 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#

terminal
$ 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.

How the pieces connect
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.

What’s next?#