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 handlersnoddde diagram— generate a flow diagram of how commands, events, and queries traverse a domain
Installation
npm install -g @noddde/cliOr use it without installing via npx:
npx @noddde/cli new project my-appCommands
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-bookingThe 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.tsSee 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-bookingGenerates 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 BankAccountGenerates:
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 OrderSummaryGenerates:
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 PaymentProcessingGenerates:
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 auctionIf 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:
| Command | Derived event |
|---|---|
PlaceBid | BidPlaced |
CreateAuction | AuctionCreated |
CloseAuction | AuctionClosed |
Submit | Submitted |
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-exportdeciders/index.ts— appends the decider re-exportevolvers/index.ts— appends the evolver re-exportauction.ts— adds imports, inserts intoDefineCommands,DefineEvents,decide, andevolve
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-summaryIf 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 inDefineQueries<{...}>(result type defaults to<View> | null)query-handlers/index.ts— appends the handler re-exportauction-summary.ts— adds the handler import and the entry inqueryHandlers
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-summaryIf you omit --projection, the CLI prompts you to pick.
Creates one new file:
And updates:
on-entries/index.ts— appends the reducer re-exportauction-summary.ts— adds the import and the entry in theonmap
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:
| Full | Alias |
|---|---|
noddde add command | noddde a c |
noddde add query | noddde a q |
noddde add event-handler | noddde a eh |
Flag reference
| Flag | Applies to | Purpose |
|---|---|---|
--aggregate | add command | Target aggregate name (interactive picker if omitted) |
--projection | add query, add event-handler | Target projection name (interactive picker if omitted) |
--event | add command | Override 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 onlyOutput formats
| Format | Use case |
|---|---|
mermaid | Default. Renders inline on GitHub, in Fumadocs, and in any Mermaid-aware previewer |
dot | Graphviz DOT — pipe to dot -Tsvg for high-resolution SVG |
json | Canonical 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
endFlag reference
| Flag | Default | Purpose |
|---|---|---|
--out <path> | stdout | Write the diagram to a file |
--format <format> | mermaid | One of mermaid, dot, json |
--scope <scope> | all | One of write, read, process, all — limits the subgraphs |
--hide-isolated | off | Drop nodes with degree 0 after scope filtering |
--tsconfig <path> | nearest ancestor | Path 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
externalstyle and the warning names both ends of the broken edge. This commonly catches typos in the saga'scommandsunion or aggregates that haven't been wired in yet. - Unresolvable saga commands — a saga's
commandsfield doesn't resolve to a finite union of literal-named members (e.g. it's typed asCommandrather 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)sagasas named records, or- a
definitionobject produced bydefineDomain(...).
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_caseAll 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.