zmdbzero-maintenance data layer
Docs Benchmarks Anti-patterns OpenAPI
Docs / Build an application

CLI & ScaffoldingSupported

zmdb new creates projects and application components. zmdb generate keeps its separate meaning: generate a migration from the declared schema. zmdb embed packages SQLite migration SQL into a filesystem-free TypeScript module.

Start a project#

This transcript is from the real package bin:

$ yarn zmdb new project blog
created blog/package.json
created blog/tsconfig.json
created blog/scripts/build.mjs
created blog/vitest.config.ts
created blog/zmdb.config.ts
created blog/src/app.module.ts
created blog/src/main.ts
created blog/src/health.controller.ts
created blog/src/health.controller.spec.ts
created blog/.gitignore

The generated tree is exactly:

blog/
├── .gitignore
├── package.json
├── scripts/
│   └── build.mjs
├── src/
│   ├── app.module.ts
│   ├── health.controller.spec.ts
│   ├── health.controller.ts
│   └── main.ts
├── tsconfig.json
├── vitest.config.ts
└── zmdb.config.ts

It is a runnable SQLite application, not a collection of placeholders. The config opens a file-backed database, the health test drives the real web test application and database driver, and the AOT adapter is used for both application and test builds.

After installing dependencies, the generated scripts exercise the same classes of gate as this repository:

cd blog
yarn install
yarn check
yarn build
yarn start

check runs formatting, TypeScript, lint, the AOT test build, and Vitest.

Add application components#

The six scaffold kinds are project, schema, controller, module, repository, and command:

$ yarn zmdb new schema post
created src/post.ts
created src/post.spec.ts

$ yarn zmdb new controller posts
created src/posts.controller.ts
created src/posts.controller.spec.ts

add to src/app.module.ts, in @Module({ controllers: [ … ] }):
  PostsController,

$ yarn zmdb new module billing
created src/billing.module.ts
created src/billing.module.spec.ts

add to src/app.module.ts, in @Module({ imports: [ … ] }):
  BillingModule,

$ yarn zmdb new repository post
created src/post.repository.ts
created src/post.repository.spec.ts

add to src/app.module.ts, in @Module({ providers: [ … ] }):
  postRepositoryProvider(driver),

$ yarn zmdb new command import-posts
created src/import-posts.command.ts
created src/import-posts.command.spec.ts

add to src/app.module.ts, in @Module({ commands: [ … ] }):
  ImportPostsCommand,

Those commands add this measured file set:

src/
├── billing.module.spec.ts
├── billing.module.ts
├── import-posts.command.spec.ts
├── import-posts.command.ts
├── post.repository.spec.ts
├── post.repository.ts
├── post.spec.ts
├── post.ts
├── posts.controller.spec.ts
└── posts.controller.ts

Every scaffold that contains behaviour includes a behavioural spec. The schema spec also contains an AOT canary, so a package that forgot the transformer fails in its tests instead of accepting unchecked input.

Safety properties#

These are structural rules in the implementation and acceptance tests, not recommendations for template authors.

Why the generated source stays small#

A controller remains ordinary framework code:

@Controller('/posts')
export class PostsController {
  @Get()
  list(): { readonly resource: string; readonly items: readonly unknown[] } {
    return { resource: 'posts', items: [] };
  }
}

The useful generated material is the route test and the explicit module wiring, not a second abstraction over controllers. A schema is still one interface; DTOs, JSON Schema, DDL, and validators derive from it rather than becoming more generated files.

The rest of the executable#

The same bin also owns migrations, checks, catalog pull, DDL export, module inspection, the REPL, and the read-only Studio:

yarn zmdb --help
yarn zmdb generate --name add_posts
yarn zmdb embed
yarn zmdb migrate
yarn zmdb check
yarn zmdb studio

See the CLI overview for the complete command and exit-code reference.

---

See also: Monorepos & Libraries · Building CLI Applications · studio