@tactical-ddd/nx
v0.1.2
Published
[](https://www.npmjs.com/package/@tactical-ddd/nx) [](https://www.npmjs.com/package/@tactical-ddd/nx) [ architectures inside your Nx monorepo.
The suite is being built out incrementally. Generators currently available:
init— the recommended starting point: bootstraps the whole workspace (generator defaults, module-boundary lint rules, the Shared Kernel, and an optional framework preset — React or React Native) in one step.shared-kernel— scaffolds the Shared Kernel, the agnostic foundation reused by every other module.domain— scaffolds a bounded business domain (itscontracts/core/ui/featureslayers) and wires up cross-domain isolation.
Init Generator
The init generator is the one-shot bootstrap for a Tactical DDD workspace. Run it once, right after creating your Nx workspace, and it wires up everything the rest of the ecosystem relies on. It is a composing generator — it does not duplicate logic, but orchestrates the lower-level pieces:
Workspace generator defaults — persists shared options into
nx.jsonso they are configured once and Nx injects them automatically into later generator invocations, so you never have to retype them. Two groups are written: the organizationprefixpluslinter/unitTestRunnerfor our own@tactical-ddd/nxgenerators, and the samebundler/linter/unitTestRunnerchoices for the built-in@nx/js:librarygenerator — so even a hand-rollednx g @nx/js:libraryalready matches the conventions. The matching framework library defaults are added only under the corresponding preset:@nx/react:libraryforreact,@nx/react-native:libraryforreact-native(the latter without abundler— Metro is fixed — and withjest/noneas the only test runners):// nx.json { "generators": { "@tactical-ddd/nx": { "shared-kernel": { "prefix": "@my-org", "linter": "eslint", "unitTestRunner": "jest", }, }, "@nx/js:library": { "bundler": "none", "linter": "eslint", "unitTestRunner": "jest", }, // Added only with `--preset=react`: "@nx/react:library": { "bundler": "none", "linter": "eslint", "unitTestRunner": "jest", }, // Added only with `--preset=react-native` (no `bundler` — Metro is fixed): "@nx/react-native:library": { "linter": "eslint", "unitTestRunner": "jest", }, }, }Module-boundary lint rules — populates
@nx/enforce-module-boundariesin the root ESLint config with the full Tactical DDD dependency graph (depConstraints). This enforces the allowed dependency directions between scopes/layers and the absence of circular dependencies. Both flat config (eslint.config.*) and legacy.eslintrc.*are detected and updated via AST manipulation. See Module Boundaries & Isolation Rules.Shared Kernel — invokes the
shared-kernelgenerator to scaffoldlibs/shared/{contracts,utils,infrastructure}.Framework preset (optional) — configures the workspace for a UI framework:
--preset=reactsets it up for React (web): the@nx/react:librarydefaults above are written, the@nx/reactgenerator plugin is added, and the React runtime —react,react-domand the@tactical-ddd/reactbindings — is installed as a production dependency.--preset=react-nativesets it up for React Native: the@nx/react-native:librarydefaults above are written, the@nx/react-nativegenerator plugin is added, and the native runtime —reactandreact-native— is installed as a production dependency. Noreact-dom(React Native renders to native views, not the DOM) and no@tactical-ddd/reactweb bindings.
With the default
--preset=nonethe workspace stays framework-agnostic and no framework tooling is pulled in. In every case the framework runtime is added only when the workspace manages none of it yet, so an existing (possibly pinned)react/react-dom/react-nativeis never re-resolved or overwritten.Dependency check & install — ensures the packages the configured and invoked generators rely on are present in the workspace
package.json, installing any that are missing (viaaddDependenciesToPackageJson). Nx plugin versions are pinned to the workspace's Nx version. The set is scoped to your choices:@nx/jsalways;@nx/eslint+@nx/eslint-pluginwhenlinter: eslint;@nx/jestor@nx/viteto matchunitTestRunner; and the React (web) or React Native packages above whenpreset: react/preset: react-native. Already-installed packages are left untouched (never downgraded).
Order matters and is handled for you: the Shared Kernel is generated first (in a fresh workspace the root ESLint config only exists after the first library is created), then the module-boundary rules are applied to it.
Usage
nx g @tactical-ddd/nx:init --prefix=@my-org --linter=eslint --unitTestRunner=jestFor a React workspace, add the preset to also install @tactical-ddd/react and the React tooling:
nx g @tactical-ddd/nx:init --prefix=@my-org --linter=eslint --unitTestRunner=jest --preset=reactFor a React Native workspace, use the react-native preset instead — it installs the @nx/react-native tooling and the react/react-native runtime:
nx g @tactical-ddd/nx:init --prefix=@my-org --linter=eslint --unitTestRunner=jest --preset=react-nativeWhen run interactively (or via Nx Console), the generator prompts for any required option that is not passed on the command line.
Options
| Option | Type | Default | Required | Description |
| ----------------- | -------- | ------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| prefix | string | — | Yes | Organization prefix used for the generated library names, e.g. @my-org. Set once, reused by all generators. |
| sharedDirectory | string | libs/shared | No | Root folder the Shared Kernel libraries are generated into. |
| linter | string | — | Yes | Linter to configure for the generated libraries. One of eslint, none. |
| unitTestRunner | string | — | Yes | Unit test runner to set up. One of jest, vitest, none. |
| bundler | string | none | No | Bundler used to build the libraries. One of none, swc, tsc, rollup, vite, esbuild. |
| preset | string | none | No | Framework preset. react installs @tactical-ddd/react + the React (web) runtime and generator defaults; react-native installs @nx/react-native + the react/react-native runtime and generator defaults. One of none, react, react-native. |
The generator is idempotent: re-running it refreshes the
nx.jsondefaults and module-boundary rules and safely skips Shared Kernel libraries that already exist.If the linter is set to
none(no ESLint config in the workspace), the module-boundary step is skipped with a warning — there is no linter to enforce the graph.
Shared Kernel Generator
The shared-kernel generator is designed to automatically deploy the Shared Kernel within your Nx monorepo. This forms the absolute foundation of the entire application architecture, containing completely agnostic (not tied to any specific business domain) code that is reused across all other modules in the system.
Usage
To run the generator, execute the following command in the root of your workspace:
nx g @tactical-ddd/nx:shared-kernelThe generator checks for the existence of libraries before creating them, making it safe to run multiple times to restore missing kernel layers.
Options
| Option | Type | Default | Required | Description |
| ---------------- | -------- | ------------- | -------- | ------------------------------------------------------------------------------------------------- |
| directory | string | libs/shared | Yes | Root folder the shared kernel libraries are generated into. Keeping libs/shared is recommended. |
| prefix | string | — | No | Organization prefix used for the generated import paths, e.g. @my-org. |
| linter | string | — | Yes | Linter to configure for the generated libraries. One of eslint, none. |
| unitTestRunner | string | — | Yes | Unit test runner to set up. One of jest, vitest, none. |
| bundler | string | none | No | Bundler used to build the libraries. One of none, swc, tsc, rollup, vite, esbuild. |
When run interactively (or via Nx Console), the generator prompts for any required option that is not passed on the command line. Example with explicit flags:
nx g @tactical-ddd/nx:shared-kernel \
--directory=libs/shared \
--prefix=@my-org \
--linter=eslint \
--unitTestRunner=jest \
--bundler=noneNote: the
contractslibrary is always generated without a unit test runner — it holds only compile-time types — regardless of theunitTestRunnervalue, which applies toutilsandinfrastructure.
Architecture & Folder Structure
The generator creates three core libraries inside the libs/shared/ directory:
libs/shared/
├── contracts/ # Global types, DTOs, and interfaces (no implementation)
├── utils/ # Pure helpers and utility functions
└── infrastructure/ # API clients, storage adapters, and loggers1. 📦 contracts
Nx Tags: scope:shared, type:contracts
This is the lowest and most abstract layer of the application. It establishes the "social contracts" between different parts of the system and the backend.
- What's inside: Global TypeScript interfaces, data types, validation schemas (Zod/Yup), API response shapes (DTOs), and global domain event structures.
- Default Interfaces: Ships with two foundational infrastructure contracts —
HttpClientandStore(see Default Contracts below). - Features: This library is generated without a unit test runner (
unitTestRunner: 'none') because it contains strictly compile-time types and interfaces that carry no executable logic. - Import Rule: It is strictly forbidden to import anything into this library from any other modules in the workspace.
Default Contracts
The contracts library is not generated empty — it is seeded with two foundational infrastructure contracts that the rest of the architecture (notably libs/shared/infrastructure) is expected to implement. Each interface is paired with a DI token (Symbol.for(...)), so it can be bound and resolved through a dependency-injection container (e.g. Inversify) without leaking the concrete implementation. Both are re-exported from the library barrel (index.ts):
import { HttpClient, HttpClientOptions, Store } from '@my-org/shared-contracts';| Interface | DI Token | Purpose |
| ------------ | -------------- | ------------------------------------------------------------------------------- |
| HttpClient | HttpClient.$ | Transport-agnostic HTTP contract (get / post / put / patch / delete). |
| Store | Store.$ | Async key/value persistence contract (set / get / delete). |
HttpClient
A framework- and library-agnostic HTTP boundary. Concrete implementations (Axios, Fetch, etc.) live in libs/shared/infrastructure; consumers depend only on this interface. The companion HttpClientOptions type carries per-request settings (e.g. timeout).
export type HttpClientOptions = {
timeout: number;
};
export interface HttpClient {
get<T>(url: string, options?: HttpClientOptions): Promise<T>;
post<T, K = unknown>(
url: string,
data?: K,
options?: HttpClientOptions,
): Promise<T>;
put<T, K = unknown>(
url: string,
data?: K,
options?: HttpClientOptions,
): Promise<T>;
patch<T, K = unknown>(
url: string,
data?: K,
options?: HttpClientOptions,
): Promise<T>;
delete<T>(url: string, options?: HttpClientOptions): Promise<T>;
}
// DI token — bind your concrete client to this symbol.
export const HttpClient = {
$: Symbol.for('HttpClient'),
};Store
A generic, async key/value persistence boundary. Back it with localStorage, IndexedDB, an in-memory map, or any remote store — the contract stays the same.
export interface Store {
set<T>(key: string, value: T): Promise<boolean>;
get<T>(key: string): Promise<T | null>;
delete(service: string): Promise<boolean>;
}
// DI token — bind your concrete store to this symbol.
export const Store = {
$: Symbol.for('Store'),
};2. 🛠 utils
Nx Tags: scope:shared, type:utils
A layer of general-purpose utilities designed to solve purely technical computation and data transformation tasks.
- What's inside: Pure functions for strings, arrays, date formatting, mathematical calculations, and custom domain-agnostic RxJS operators.
- Features: Functions in this library must not have side effects and must remain decoupled from the application state.
- Import Rule: Can only import types from
libs/shared/contracts.
3. 🌐 infrastructure
Nx Tags: scope:shared, type:infrastructure
The I/O (Input/Output) implementation layer that integrates your system with the outside world. This is the technical heart of your application's infrastructure.
- What's inside:
- HTTP/WebSocket client configurations (Axios instances, Fetch wrappers).
- Base wrappers for browser storage (localStorage, IndexedDB).
- Third-party service integrations (Sentry clients for logging, analytics trackers).
- State management configurations (
@tanstack/query-core/ base cache providers).
- Import Rule: Can freely import contracts from
libs/shared/contractsand pure helpers fromlibs/shared/utils.
Module Boundaries & Isolation Rules
The generator automatically tags these projects with scope:shared. In your root linter configuration (eslint.config.js), strict boundaries are enforced for these tags:
- Isolation from Business Logic: Code inside
libs/shared/*never and under no circumstances can import code from business domains (libs/auth/*,libs/payments/*, etc.). The shared kernel is completely isolated from business logic. - Linear Layer Dependencies: A strict hierarchy is maintained within the kernel itself:
contractsknows about no one.utilsonly knows aboutcontracts.infrastructureknows about bothcontractsandutils.
Domain Generator
The domain generator scaffolds a single bounded business domain (e.g. auth, payments, beneficiaries) under libs/[domain-name]/. Each domain is isolated from every other domain and depends only on the Shared Kernel and — for collaboration — the public contracts of other domains.
It generates one library per selected layer, tags each with scope:domain plus its layer type and a domain tag (domain:<name>), and registers a per-domain module-boundary constraint that enforces the isolation described below.
Layers
The --layers option selects which layers to generate (default: contracts,core):
| Layer | Tags | Purpose |
| ----------- | -------------------------------- | ----------------------------------------------------------------------------------------- |
| contracts | scope:domain, type:contracts | Domain-specific types, events, and ports/API boundaries. The domain's public surface. |
| core | scope:domain, type:core | Pure business logic — entities, value objects, use cases, repository interfaces. |
| ui | scope:domain, type:ui | Presentational components. Generated with the framework matching --preset (see below). |
| features | scope:domain, type:features | UI + state, framework bindings, DI containers — the topmost layer. |
The
--presetchosen (inherited from thenx.jsondefaults written byinit) decides how theui/featureslayers are generated:
react— React (web) libraries via@nx/react; a DOM lib is left in the librarytsconfig.react-native— React Native libraries via@nx/react-native(native views, no DOM lib).none(default) — framework-agnostic@nx/jslibraries, with a DOM lib added to thetsconfigfor browser globals.The framework generator (
@nx/react/@nx/react-native) is loaded lazily only when its preset is selected, so the plugin never hard-depends on either. The matching runtime (react/react-domorreact/react-native) is added centrally, only when the workspace manages none of it yet.
Cross-domain isolation (published-language)
The generator injects a per-domain @nx/enforce-module-boundaries constraint so that a domain:<name> library may depend only on:
- its own domain (
domain:<name>), - the Shared Kernel (
scope:shared), - and the public contracts of any other domain (
type:contracts).
This is the published-language pattern: a domain may depend on another domain's abstraction (its contracts), but never on its implementation (core/ui/features/infrastructure). The implementation stays hidden inside the domain and is provided across boundaries via dependency injection wired up in the composition root. Importing another domain's core fails the lint boundary check.
Usage
nx g @tactical-ddd/nx:domain payments --directory=libs/paymentsprefix, linter, unitTestRunner and preset are inherited from the nx.json defaults written by init, so in a bootstrapped workspace only the domain name (and directory) are needed. Example with explicit flags:
nx g @tactical-ddd/nx:domain payments \
--directory=libs/payments \
--prefix=@my-org \
--layers=contracts,core,features \
--preset=react \
--linter=eslint \
--unitTestRunner=jest \
--bundler=tscOptions
| Option | Type | Default | Required | Description |
| ---------------- | ---------- | ---------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| name | string | — | Yes | Domain name, also the domain:<name> tag. Passed as the first positional argument. |
| directory | string | — | Yes | Root folder the domain libraries are generated into, e.g. libs/payments. |
| layers | string[] | contracts,core | Yes | Architectural layers to generate. Any of contracts, core, ui, features. |
| prefix | string | — | No | Organization prefix used for the generated library names, e.g. @my-org. |
| preset | string | none | No | Framework preset for the ui/features layers. react uses @nx/react (web); react-native uses @nx/react-native (native). One of none, react, react-native. |
| linter | string | — | Yes | Linter to configure for the generated libraries. One of eslint, none. |
| unitTestRunner | string | — | No | Unit test runner to set up. One of jest, vitest, none. (contracts is always generated without one.) |
| bundler | string | tsc | No | Bundler used to build the libraries. One of none, swc, tsc, rollup, vite, esbuild. |
The generator is idempotent: it checks for each layer's library before creating it, so re-running it safely fills in only the layers that are missing.
If
libs/shared/contractsdoes not exist yet, the generator warns you to runinit(orshared-kernel) first, since domain libraries depend on the Shared Kernel.
