noddde

CLI

Scaffold and extend noddde projects, domains, aggregates, projections, and sagas from the command line

The @noddde/cli package provides generators that scaffold noddde modules with the correct folder structure, type definitions, and wiring code. Three command groups are available:

  • noddde new — create new modules from scratch (projects, domains, aggregates, projections, sagas)
  • noddde add — extend existing aggregates and projections with new commands, queries, or event handlers
  • noddde diagram — generate a flow diagram of how commands, events, and queries traverse a domain

Installation

npm install -g @noddde/cli

Or use it without installing via npx:

npx @noddde/cli new project my-app

Commands

All generators are under noddde new (alias noddde n).

noddde new project <name>

Scaffolds a complete runnable project with package.json, TypeScript config, a sample test, and the full domain structure inside src/.

noddde new project hotel-booking

The command prompts you to choose a persistence adapter:

? Which persistence adapter?
❯ In-memory (no external dependencies)
  Prisma (SQLite via @noddde/prisma)
  Drizzle (SQLite via @noddde/drizzle)
  TypeORM (@noddde/typeorm)

Your choice determines which dependencies are added to package.json. You can always change this later.

After scaffolding:

cd hotel-booking
npm install
npm test    # runs the sample test
npm start   # runs src/main.ts

See Project Structure for a full breakdown of the generated layout.

noddde new domain <name>

Scaffolds the domain layer only — no package.json or config files. Use this when adding a domain to an existing project.

cd src
noddde new domain hotel-booking

Generates event-model/, write-model/, read-model/, domain.ts, infrastructure/, and main.ts in the current directory.

noddde new aggregate <name>

Scaffolds a single aggregate with extracted decide handlers and evolve handlers. Run from the project root — the CLI automatically places files at src/domain/write-model/aggregates/<name>/.

noddde new aggregate BankAccount

Generates:

index.ts
state.ts
bank-account.ts

Decide handlers are typed with InferDecideHandler<Def, "CommandName"> and evolve handlers with InferEvolveHandler<Def, "EventName">. Both are standalone exported functions imported into the aggregate definition. This keeps each handler individually testable and the aggregate definition concise.

noddde new projection <name>

Scaffolds a single projection with extracted query handlers and on-entries. Run from the project root — the CLI automatically places files at src/domain/read-model/projections/<name>/.

noddde new projection OrderSummary

Generates:

index.ts
order-summary.ts

Query handlers are typed with InferProjectionQueryHandler<Def, "QueryName">. On-entries are the event reducers that build the view — they live in on-entries/ and are imported into the projection's on map.

noddde new saga <name>

Scaffolds a saga with extracted on-entries. Run from the project root — the CLI automatically places files at src/domain/process-model/<name>/.

noddde new saga PaymentProcessing

Generates:

index.ts
state.ts
saga.ts

On-entries are { id, handle } objects typed with InferSagaOnEntry<Def, "EventName"> once event types are wired. They are imported into the defineSaga on map. Each entry specifies how to extract the saga ID from the event and how to handle the state transition.

Extending Existing Modules

After initial scaffolding, you rarely work with a single Create command or Get query — aggregates grow commands over time, and projections grow queries and event handlers. The noddde add command group (alias noddde a) generates the new handler files and wires them into the existing barrels and definition file, so you don't have to touch imports, DefineCommands/DefineEvents/DefineQueries unions, or the decide/evolve/queryHandlers/on maps by hand.

All add subcommands are idempotent — running them twice leaves your files unchanged.

noddde add command <name>

Adds a command (with decider and evolver) to an existing aggregate.

noddde add command PlaceBid --aggregate auction

If you omit --aggregate, the CLI lists the aggregates it finds under src/domain/write-model/aggregates/ and prompts you to pick one.

The CLI auto-derives the event name from the command name using the convention verb + subject → subject + past-tense verb:

CommandDerived event
PlaceBidBidPlaced
CreateAuctionAuctionCreated
CloseAuctionAuctionClosed
SubmitSubmitted

You're shown the derived name and can confirm it or type a different one. Pass --event <name> to skip the prompt.

The command creates three new files under the target aggregate:

And updates four existing files:

  • commands/index.ts — appends the payload re-export
  • deciders/index.ts — appends the decider re-export
  • evolvers/index.ts — appends the evolver re-export
  • auction.ts — adds imports, inserts into DefineCommands, DefineEvents, decide, and evolve

You then fill in the payload fields, decider logic, and evolver state transition — everything else is wired for you.

noddde add query <name>

Adds a query (with handler) to an existing projection.

noddde add query ListActiveAuctions --projection auction-summary

If you omit --projection, the CLI prompts you to pick from the projections under src/domain/read-model/projections/.

Creates two new files under the projection:

And updates:

  • queries/index.ts — adds the payload import and a new entry in DefineQueries<{...}> (result type defaults to <View> | null)
  • query-handlers/index.ts — appends the handler re-export
  • auction-summary.ts — adds the handler import and the entry in queryHandlers

The generated handler is typed with InferProjectionQueryHandler<Def, "QueryName"> and defaults to views.load(query.payload.id). Adjust the payload shape and the handler body to fit your query.

noddde add event-handler <event-name>

Adds an event handler (on-entry / view reducer) to an existing projection.

noddde add event-handler BidPlaced --projection auction-summary

If you omit --projection, the CLI prompts you to pick.

Creates one new file:

And updates:

  • on-entries/index.ts — appends the reducer re-export
  • auction-summary.ts — adds the import and the entry in the on map

The generated reducer takes the event and the current view and returns a new view. Once event types are wired in the projection's events union, replace the stub signature with InferProjectionEventHandler<Def, "EventName"> for full type inference.

Aliases

All add subcommands have short aliases:

FullAlias
noddde add commandnoddde a c
noddde add querynoddde a q
noddde add event-handlernoddde a eh

Flag reference

FlagApplies toPurpose
--aggregateadd commandTarget aggregate name (interactive picker if omitted)
--projectionadd query, add event-handlerTarget projection name (interactive picker if omitted)
--eventadd commandOverride the auto-derived event name

Visualizing Domains

The noddde diagram command (alias noddde d) reads a domain module and emits a flow diagram showing how commands, events, and queries move through the system. Five of the six edge types — command → aggregate, aggregate → event, event → projection, query → projection, event → saga — are derived directly from Object.keys(...) on the loaded definition. The sixth — saga → command — is resolved through the TypeScript compiler API by reading the commands field of each saga's type bundle.

noddde diagram                                # uses src/domain/domain.ts
noddde diagram path/to/domain.ts              # explicit entry
noddde diagram --out diagram.mmd              # write to file
noddde diagram --format json                  # machine-readable graph
noddde diagram --scope process                # process-model only

Output formats

FormatUse case
mermaidDefault. Renders inline on GitHub, in Fumadocs, and in any Mermaid-aware previewer
dotGraphviz DOT — pipe to dot -Tsvg for high-resolution SVG
jsonCanonical DomainGraph shape for downstream tooling (docs sites, registries)

Diagram structure

Commands, events, and queries are pill-shaped nodes; aggregates, projections, and sagas are box nodes. Three subgraphs group them by model (Write Model, Read Model, Process Model). Solid arrows mark edges derived at runtime; dashed arrows mark edges resolved through type analysis (currently only saga → command).

For the sample-auction domain:

flowchart LR
  subgraph WM["Write Model"]
    cmdCreate(["CreateAuction"])
    cmdBid(["PlaceBid"])
    cmdClose(["CloseAuction"])
    Auction[["Auction"]]
    evCreated(["AuctionCreated"])
    evBid(["BidPlaced"])
    evRej(["BidRejected"])
    evClosed(["AuctionClosed"])
    cmdCreate --> Auction
    cmdBid --> Auction
    cmdClose --> Auction
    Auction --> evCreated
    Auction --> evBid
    Auction --> evRej
    Auction --> evClosed
  end
  subgraph RM["Read Model"]
    Summary[["AuctionSummary"]]
    qGet(["GetAuctionSummary"])
    evCreated --> Summary
    evBid --> Summary
    evClosed --> Summary
    qGet --> Summary
  end

Flag reference

FlagDefaultPurpose
--out <path>stdoutWrite the diagram to a file
--format <format>mermaidOne of mermaid, dot, json
--scope <scope>allOne of write, read, process, all — limits the subgraphs
--hide-isolatedoffDrop nodes with degree 0 after scope filtering
--tsconfig <path>nearest ancestorPath to a tsconfig.json used by the saga static-analysis pass

What gets diagnosed

The diagram doubles as a static check on the domain. Two diagnostics are surfaced as warnings on the JSON output and on stderr:

  • External commands — a saga dispatches a command name that no aggregate handles. The command node is rendered with an external style and the warning names both ends of the broken edge. This commonly catches typos in the saga's commands union or aggregates that haven't been wired in yet.
  • Unresolvable saga commands — a saga's commands field doesn't resolve to a finite union of literal-named members (e.g. it's typed as Command rather than a discriminated union). The saga still appears with its incoming event edges; the warning notes that no outgoing edges could be produced.

Loading the domain

The CLI loads the entry file via tsx's programmatic API — no prebuild required. The entry must export either:

  • aggregates, projections, and (optionally) sagas as named records, or
  • a definition object produced by defineDomain(...).

Both forms are supported by the same loader. Convention: src/domain/domain.ts carries the structural exports and has no top-level dependencies on infrastructure (DB connections, env vars). If your entry has top-level side effects, dynamic import will fail with a message pointing you to the structural module.

Name Handling

Names can be provided in any casing — the CLI normalizes them:

noddde new aggregate BankAccount     # PascalCase
noddde new aggregate bank-account    # kebab-case
noddde new aggregate bank_account    # snake_case

All three produce the same output: a bank-account/ directory with BankAccount as the PascalCase identifier in code.

Names must produce a valid TypeScript identifier (start with a letter). The CLI rejects invalid names with a clear error.

On this page