@alveolus/arch 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +11 -0
  2. package/dist/bin.mjs +4 -2
  3. package/dist/bin.mjs.map +1 -1
  4. package/dist/{cli-P5PwH9OE.mjs → docs-DcFgskuN.mjs} +214 -43
  5. package/dist/docs-DcFgskuN.mjs.map +1 -0
  6. package/dist/index.d.mts +53 -12
  7. package/dist/index.d.mts.map +1 -1
  8. package/dist/index.mjs +2 -2
  9. package/docs/core/application/command-handlers.md +617 -0
  10. package/docs/core/application/event-publishers.md +234 -0
  11. package/docs/core/application/event-translators.md +329 -0
  12. package/docs/core/application/index.md +99 -0
  13. package/docs/core/application/integration-events.md +277 -0
  14. package/docs/core/application/outbox.md +416 -0
  15. package/docs/core/application/query-handlers.md +292 -0
  16. package/docs/core/application/unit-of-work.md +352 -0
  17. package/docs/core/domain/aggregates.md +822 -0
  18. package/docs/core/domain/domain-errors.md +251 -0
  19. package/docs/core/domain/domain-events.md +292 -0
  20. package/docs/core/domain/domain-services.md +249 -0
  21. package/docs/core/domain/entities.md +431 -0
  22. package/docs/core/domain/index.md +93 -0
  23. package/docs/core/domain/ports.md +284 -0
  24. package/docs/core/domain/repositories.md +335 -0
  25. package/docs/core/domain/value-objects.md +425 -0
  26. package/docs/core/domain/views.md +265 -0
  27. package/docs/core/index.md +108 -0
  28. package/docs/core/strategic/anti-corruption-layers.md +349 -0
  29. package/docs/core/strategic/index.md +83 -0
  30. package/docs/core/strategic/open-host-services.md +287 -0
  31. package/docs/core/strategic/published-language.md +265 -0
  32. package/docs/core/utilities/result.md +413 -0
  33. package/docs/guide/agents.md +68 -0
  34. package/docs/guide/existing-project.md +108 -0
  35. package/docs/guide/getting-started.md +286 -0
  36. package/docs/guide/learning-path.md +123 -0
  37. package/docs/guide/project-layout.md +324 -0
  38. package/docs/guide/versioning.md +42 -0
  39. package/docs/integrations/index.md +112 -0
  40. package/docs/integrations/nestjs.md +169 -0
  41. package/docs/rules/index.md +185 -0
  42. package/docs/rules/layers/no-driving-shortcut.md +119 -0
  43. package/docs/rules/layers/no-impure-domain.md +191 -0
  44. package/docs/rules/layers/no-outward-import.md +186 -0
  45. package/docs/rules/layers/no-portless-adapter.md +123 -0
  46. package/docs/rules/strategic/no-cross-context-import.md +140 -0
  47. package/docs/rules/strategic/no-fat-shared-kernel.md +81 -0
  48. package/docs/rules/strategic/no-leaky-host-service.md +107 -0
  49. package/docs/rules/strategic/no-unmapped-context.md +114 -0
  50. package/docs/rules/tactical/no-aggregate-reference.md +139 -0
  51. package/docs/rules/tactical/no-foreign-command-dependency.md +119 -0
  52. package/docs/rules/tactical/no-foreign-query-dependency.md +201 -0
  53. package/docs/rules/tactical/no-loose-code.md +171 -0
  54. package/docs/rules/tactical/no-misplaced-class.md +146 -0
  55. package/docs/rules/tactical/no-public-field.md +113 -0
  56. package/docs/rules/tactical/no-stateful-service.md +102 -0
  57. package/docs/rules/tactical/no-thrown-failure.md +162 -0
  58. package/docs/rules/tooling/no-loose-disable.md +98 -0
  59. package/package.json +4 -3
  60. package/dist/cli-P5PwH9OE.mjs.map +0 -1
@@ -0,0 +1,324 @@
1
+ ---
2
+ description: "The folder structure of a Domain-Driven Design project in TypeScript: bounded contexts, domain, application, adapters and the direction between layers."
3
+ ---
4
+
5
+ # Project layout
6
+
7
+ Every Alveolus project has the same shape: the same folders, the same file names, the same
8
+ direction between layers.
9
+
10
+ <dl class="al-glance">
11
+ <dt>Unit</dt><dd>A <a href="#bounded-contexts">bounded context</a>: a folder under <code>src/</code>, such as <code>src/ordering/</code></dd>
12
+ <dt>Layers</dt><dd><a href="#layers"><code>domain/</code>, <code>application/</code>, <code>published-language/</code>, <code>driven/</code>, <code>driving/</code></a></dd>
13
+ <dt>Wired by</dt><dd><a href="#composition-root">The composition root</a>, <code>ordering.module.ts</code></dd>
14
+ <dt>Shared</dt><dd><a href="#shared-kernel"><code>src/shared-kernel/</code></a>, same shape</dd>
15
+ <dt>Checked by</dt><dd><a href="/rules/layers/no-outward-import"><code>layers/no-outward-import</code></a>, <a href="/rules/tactical/no-misplaced-class"><code>tactical/no-misplaced-class</code></a>, <a href="/rules/strategic/no-cross-context-import"><code>strategic/no-cross-context-import</code></a>, <a href="/rules/layers/no-impure-domain"><code>layers/no-impure-domain</code></a></dd>
16
+ </dl>
17
+
18
+ ## Why
19
+
20
+ A new teammate looks for the rule that refuses an empty order. Is it in the controller, a service,
21
+ a helper, the repository? An agent asked to add a rule puts it wherever the task led it. Each
22
+ project invents its own layout, and each layout erodes a little with every change.
23
+
24
+ ::: tip The fix
25
+ One layout for every project, kept by `alveolus arch check`. Knowing what a class is tells you
26
+ where it lives, and the other way round: the rule is in `domain/aggregates/order.aggregate.ts`,
27
+ because that is where it can only be.
28
+ :::
29
+
30
+ ## How it works
31
+
32
+ A bounded context is split into layers, each in its folder. The domain sits at the center, the
33
+ application around it, the adapters at the edge; the composition root wires them together.
34
+
35
+ <div class="al-diagram">
36
+ <svg viewBox="0 0 680 380" role="img" aria-label="A bounded context. Its composition root wires four layers. Driving adapters call the application, the application uses the domain, driven adapters implement the ports of the domain. The published language is the format exchanged with other contexts. Every dependency points towards the domain.">
37
+ <defs>
38
+ <marker id="layout-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
39
+ <path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
40
+ </marker>
41
+ </defs>
42
+ <rect class="boundary" x="8" y="8" width="664" height="364" rx="14" />
43
+ <text class="note" x="24" y="32">src/ordering/ · a bounded context</text>
44
+ <rect class="box" x="200" y="46" width="280" height="48" rx="8" />
45
+ <text class="label" x="340" y="68" text-anchor="middle">composition root</text>
46
+ <text class="note" x="340" y="85" text-anchor="middle">ordering.module.ts · wires it all</text>
47
+ <rect class="box" x="28" y="128" width="150" height="150" rx="8" />
48
+ <text class="label" x="103" y="160" text-anchor="middle">driving/</text>
49
+ <text class="note" x="103" y="184" text-anchor="middle">controllers</text>
50
+ <text class="note" x="103" y="202" text-anchor="middle">consumers</text>
51
+ <text class="note" x="103" y="220" text-anchor="middle">jobs, CLI…</text>
52
+ <text class="note" x="103" y="258" text-anchor="middle">calls use cases</text>
53
+ <rect class="box" x="502" y="128" width="150" height="150" rx="8" />
54
+ <text class="label" x="577" y="160" text-anchor="middle">driven/</text>
55
+ <text class="note" x="577" y="184" text-anchor="middle">repositories</text>
56
+ <text class="note" x="577" y="202" text-anchor="middle">API clients</text>
57
+ <text class="note" x="577" y="220" text-anchor="middle">outbox, clock…</text>
58
+ <text class="note" x="577" y="258" text-anchor="middle">implements ports</text>
59
+ <rect class="box" x="210" y="118" width="260" height="170" rx="10" />
60
+ <text class="label" x="340" y="142" text-anchor="middle">application/</text>
61
+ <text class="note" x="340" y="160" text-anchor="middle">commands · queries</text>
62
+ <rect class="boundary" x="228" y="176" width="224" height="96" rx="8" />
63
+ <text class="label" x="340" y="204" text-anchor="middle">domain/</text>
64
+ <text class="note" x="340" y="224" text-anchor="middle">aggregates · entities</text>
65
+ <text class="note" x="340" y="242" text-anchor="middle">value objects · events</text>
66
+ <text class="note" x="340" y="260" text-anchor="middle">errors · services · ports</text>
67
+ <rect class="box" x="230" y="314" width="220" height="44" rx="8" />
68
+ <text class="label" x="340" y="334" text-anchor="middle">published-language/</text>
69
+ <text class="note" x="340" y="350" text-anchor="middle">what other contexts read</text>
70
+ <path class="link" d="M 178 203 L 208 203" marker-end="url(#layout-arrow)" />
71
+ <path class="link" d="M 502 224 L 454 224" marker-end="url(#layout-arrow)" />
72
+ <path class="link" d="M 340 288 L 340 312" marker-end="url(#layout-arrow)" />
73
+ <path class="link" d="M 103 278 L 103 336 L 228 336" marker-end="url(#layout-arrow)" />
74
+ <path class="link" d="M 577 278 L 577 336 L 452 336" marker-end="url(#layout-arrow)" />
75
+ <path class="link" d="M 230 94 L 140 126" marker-end="url(#layout-arrow)" />
76
+ <path class="link" d="M 450 94 L 540 126" marker-end="url(#layout-arrow)" />
77
+ <path class="link" d="M 340 94 L 340 116" marker-end="url(#layout-arrow)" />
78
+ </svg>
79
+ </div>
80
+
81
+ Arrows read "depends on". They all point inwards: the domain depends on nothing but itself, so the
82
+ business rules never change because a database, a framework or another context did.
83
+
84
+ ## The tree
85
+
86
+ ```
87
+ src/
88
+ main.ts # starts the application
89
+ app.module.ts # assembles the bounded contexts
90
+ ordering/ # a bounded context
91
+ ordering.module.ts # its composition root
92
+ domain/
93
+ aggregates/ # order.aggregate.ts
94
+ entities/ # order-line.entity.ts
95
+ value-objects/ # order-id.identifier.ts
96
+ events/ # order-placed.event.ts
97
+ errors/ # invalid-total.error.ts
98
+ services/ # shipping-cost.service.ts
99
+ repositories/ # orders.repository.ts
100
+ ports/ # price-list.port.ts
101
+ views/ # order-summary.view.ts
102
+ application/
103
+ commands/ # place-order.command.ts
104
+ queries/ # get-order-summary.query.ts
105
+ translators/ # order-events.translator.ts
106
+ published-language/ # order-placed.representation.ts
107
+ driven/
108
+ pg/
109
+ adapters/ # pg-orders.adapter.ts
110
+ catalog/
111
+ adapters/ # catalog-price-list.adapter.ts
112
+ driving/
113
+ http/
114
+ controllers/ # orders.controller.ts
115
+ rabbitmq/
116
+ consumers/ # payment-received.consumer.ts
117
+ catalog/ # another bounded context, same shape
118
+ shared-kernel/ # shared by every bounded context, same shape
119
+ ```
120
+
121
+ Only create a folder when it gets its first file: a small context may have no `entities/`,
122
+ `services/` or `driving/rabbitmq/`. No other folder is expected: a file directly in `domain/`, in a
123
+ folder such as `domain/helpers/`, or one level too deep is reported by
124
+ [`layers/no-outward-import`](../rules/layers/no-outward-import.md#folders-inside-a-layer).
125
+
126
+ ## Bounded contexts
127
+
128
+ A bounded context is a folder under `src/`, declared in
129
+ [`alveolus.config.ts`](./getting-started.md#configure-the-checks). Contexts may be nested, for
130
+ instance under `src/modules/`. Each one has its own model: a `Product` in the catalog and a product
131
+ in ordering are two different things, and neither imports the other.
132
+
133
+ <div class="al-cards">
134
+ <div class="al-card"><span class="al-card-title">Closed</span>The only class another context may import is its <a href="../core/strategic/open-host-services">open host service</a>.</div>
135
+ <div class="al-card"><span class="al-card-title">Entered at one place</span>Only an <a href="../core/strategic/anti-corruption-layers">anti-corruption layer</a> or the composition root may import that service.</div>
136
+ <div class="al-card"><span class="al-card-title">Talking in JSON</span>What crosses the boundary is the <a href="../core/strategic/published-language">published language</a>, redeclared by the reader, never the classes of the other model.</div>
137
+ </div>
138
+
139
+ <div class="al-diagram">
140
+ <svg viewBox="0 0 680 230" role="img" aria-label="Two bounded contexts. In ordering, the anti-corruption layer CatalogPriceList implements the PriceList port of the domain and imports CatalogApi, the open host service of catalog. A direct import from the ordering domain to the catalog domain is forbidden.">
141
+ <defs>
142
+ <marker id="boundary-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
143
+ <path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
144
+ </marker>
145
+ </defs>
146
+ <rect class="boundary" x="8" y="8" width="300" height="214" rx="14" />
147
+ <text class="note" x="24" y="32">src/catalog/</text>
148
+ <rect class="box" x="30" y="50" width="256" height="58" rx="8" />
149
+ <text class="label" x="158" y="74" text-anchor="middle">CatalogApi</text>
150
+ <text class="note" x="158" y="94" text-anchor="middle">driving/ · OpenHostService</text>
151
+ <rect class="box" x="30" y="146" width="256" height="58" rx="8" />
152
+ <text class="label" x="158" y="170" text-anchor="middle">Product</text>
153
+ <text class="note" x="158" y="190" text-anchor="middle">domain/aggregates/</text>
154
+ <rect class="boundary" x="372" y="8" width="300" height="214" rx="14" />
155
+ <text class="note" x="388" y="32">src/ordering/</text>
156
+ <rect class="box" x="394" y="50" width="256" height="58" rx="8" />
157
+ <text class="label" x="522" y="74" text-anchor="middle">CatalogPriceList</text>
158
+ <text class="note" x="522" y="94" text-anchor="middle">driven/ · AntiCorruptionLayer</text>
159
+ <rect class="box" x="394" y="146" width="256" height="58" rx="8" />
160
+ <text class="label" x="522" y="170" text-anchor="middle">PriceList</text>
161
+ <text class="note" x="522" y="190" text-anchor="middle">domain/ports/</text>
162
+ <path class="link" d="M 394 79 L 288 79" marker-end="url(#boundary-arrow)" />
163
+ <text class="note" x="341" y="70" text-anchor="middle">imports</text>
164
+ <path class="link" d="M 522 108 L 522 144" marker-end="url(#boundary-arrow)" />
165
+ <text class="note" x="530" y="131">extends</text>
166
+ <path class="link" d="M 394 175 L 288 175" stroke-dasharray="4 4" marker-end="url(#boundary-arrow)" />
167
+ <text class="label" x="341" y="166" text-anchor="middle">✕</text>
168
+ <text class="note" x="341" y="196" text-anchor="middle">never</text>
169
+ </svg>
170
+ </div>
171
+
172
+ The ordering domain asks for prices in its own words, through the `PriceList` port. The
173
+ anti-corruption layer is the one place that knows the catalog exists: it calls `CatalogApi`,
174
+ reads its JSON and answers with ordering's objects. If the catalog moves behind HTTP, only that
175
+ adapter changes. Checked by [`strategic/no-cross-context-import`](../rules/strategic/no-cross-context-import.md).
176
+
177
+ ### Core, supporting, generic
178
+
179
+ Not every bounded context deserves the same investment. Vernon classifies the subdomains a system
180
+ covers, and a bounded context implements one of them: the **core domain** is what the business
181
+ competes on and gets the best developers and the full tactical model; a **supporting subdomain**
182
+ is needed but not distinctive, written in house with a lighter hand; a **generic subdomain** is
183
+ bought, taken off the shelf or wrapped. `alveolus.config.ts` says which is which, under
184
+ `subdomains`, and every context is listed once.
185
+
186
+ <div class="al-cards">
187
+ <div class="al-card"><span class="al-card-title">Core</span>Every rule applies: the layers, the building blocks, the boundary.</div>
188
+ <div class="al-card"><span class="al-card-title">Supporting and generic</span>Only the <a href="/rules/#where-a-rule-applies">boundary rules</a> apply: the context is closed, reached through its open host service, and consumes the others through theirs. Inside, any layout and any code.</div>
189
+ <div class="al-card"><span class="al-card-title">Shared kernel</span>Every rule applies: what it holds reaches every context.</div>
190
+ </div>
191
+
192
+ A supporting or generic context needs no layers, no composition root and no building block. The
193
+ one thing it marks is its open host service, when another context calls it. When it consumes the
194
+ core, it imports the open host service from anywhere; only a core context has to translate what it
195
+ consumes in an anti-corruption layer, because only it has a model to protect.
196
+
197
+ ## Layers
198
+
199
+ <div class="al-cards">
200
+ <div class="al-card"><span class="al-card-title"><code>domain/</code></span>The model: aggregates, entities, value objects, events, errors, domain services, and the ports and repositories it needs. No framework, no ORM.</div>
201
+ <div class="al-card"><span class="al-card-title"><code>application/</code></span>One class per use case: command handlers, query handlers, and the translators that turn domain events into the published language.</div>
202
+ <div class="al-card"><span class="al-card-title"><code>published-language/</code></span>The JSON types exchanged with other contexts: what this context publishes, and what it reads from the others.</div>
203
+ <div class="al-card"><span class="al-card-title"><code>driven/</code></span>The adapters that implement the ports: database repositories, API clients, the outbox, the clock.</div>
204
+ <div class="al-card"><span class="al-card-title"><code>driving/</code></span>The adapters that call the use cases: HTTP controllers, message consumers, scheduled jobs, CLI commands.</div>
205
+ </div>
206
+
207
+ ### Who may import what
208
+
209
+ Read a row as "files in this layer may import…", within the same bounded context or from the
210
+ shared kernel.
211
+
212
+ | From ↓ · To → | domain | application | published-language | driven | driving | composition root |
213
+ | --- | :-: | :-: | :-: | :-: | :-: | :-: |
214
+ | **domain** | ✓ | ✕ | ✕ | ✕ | ✕ | ✕ |
215
+ | **application** | ✓ | ✓ | ✓ | ✕ | ✕ | ✕ |
216
+ | **published-language** | ✕ | ✕ | ✓ | ✕ | ✕ | ✕ |
217
+ | **driven** | ✓ | ✓ | ✓ | ✓ | ✕ | ✕ |
218
+ | **driving** | ✓ | ✓ | ✓ | ✕ | ✓ | ✕ |
219
+ | **composition root** | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
220
+
221
+ A driving adapter calls the handlers of the application: it imports no repository, port, aggregate
222
+ or domain service of the domain, checked by
223
+ [`layers/no-driving-shortcut`](../rules/layers/no-driving-shortcut.md).
224
+
225
+ Outside the project, the domain may import the domain building blocks of `@alveolus/core` and the
226
+ packages listed in `domainDependencies`; the application adds the rest of `@alveolus/core` and
227
+ `applicationDependencies`; the published language may import the published-language types of core
228
+ and packages such as a schema library; adapters may import any package. Checked by
229
+ [`layers/no-outward-import`](../rules/layers/no-outward-import.md) and [`layers/no-impure-domain`](../rules/layers/no-impure-domain.md).
230
+
231
+ ### Adapters by technology
232
+
233
+ Inside `driven/` and `driving/`, files always sit under the name of their technology:
234
+ `driven/pg/adapters/`, `driven/http/adapters/`, `driving/http/controllers/`. Replacing a
235
+ technology then means adding a folder next to the old one, never touching it. An adapter that calls
236
+ another bounded context in the same process sits under the name of that context:
237
+ `driven/catalog/adapters/`. Every class in `driven/<technology>/adapters/` extends a port of the
238
+ domain: checked by [`layers/no-portless-adapter`](../rules/layers/no-portless-adapter.md).
239
+
240
+ ::: tip
241
+ Inside `domain/` and `application/`, every class extends a building block of `@alveolus/core`:
242
+ there are no free functions, no plain classes and no module state. Checked by
243
+ [`tactical/no-loose-code`](../rules/tactical/no-loose-code.md).
244
+ :::
245
+
246
+ ## Folders and file names
247
+
248
+ Each class goes in the folder of its kind, in a file whose name ends with that kind. One class per
249
+ file; the types that belong to it, such as its snapshot or its command input, stay in its file.
250
+ Checked by [`tactical/no-misplaced-class`](../rules/tactical/no-misplaced-class.md).
251
+
252
+ ### In `domain/`
253
+
254
+ | Kind | Extends | Folder | File name |
255
+ | --- | --- | --- | --- |
256
+ | Aggregate | `AggregateRoot` | `aggregates/` | `order.aggregate.ts` |
257
+ | Entity | `Entity` | `entities/` | `order-line.entity.ts` |
258
+ | Value object | `ValueObject` | `value-objects/` | `money.value-object.ts` |
259
+ | Identifier | `Identifier` | `value-objects/` | `order-id.identifier.ts` |
260
+ | Domain event | `DomainEvent` | `events/` | `order-placed.event.ts` |
261
+ | Domain error | `DomainError` | `errors/` | `invalid-total.error.ts` |
262
+ | Domain service | `DomainService` | `services/` | `shipping-cost.service.ts` |
263
+ | Repository | `CommandRepository`<br>`QueryRepository` | `repositories/` | `orders.repository.ts` |
264
+ | Port | `Port` | `ports/` | `price-list.port.ts` |
265
+ | View | `View<…>` type | `views/` | `order-summary.view.ts` |
266
+
267
+ ### In `application/`
268
+
269
+ | Kind | Extends | Folder | File name |
270
+ | --- | --- | --- | --- |
271
+ | Command handler | `CommandHandler` | `commands/` | `place-order.command.ts` |
272
+ | Query handler | `QueryHandler` | `queries/` | `get-order-summary.query.ts` |
273
+ | Event translator | `EventTranslator` | `translators/` | `order-events.translator.ts` |
274
+
275
+ ### Around them
276
+
277
+ | Kind | Folder | File name |
278
+ | --- | --- | --- |
279
+ | Representation | `published-language/` | `order-placed.representation.ts` |
280
+ | Driven adapter | `driven/pg/adapters/` | `pg-orders.adapter.ts` |
281
+ | Open host service | `driving/<technology>/` | free |
282
+
283
+ A representation is a `PublishedLanguage<…>` type, a driven adapter extends a port, an open host
284
+ service implements `OpenHostService`.
285
+
286
+ Tests sit next to the code they test and keep its name: `order.aggregate.spec.ts` or
287
+ `order.aggregate.test.ts`.
288
+
289
+ ## Composition root
290
+
291
+ Each bounded context has one file at its root that wires its adapters into its use cases, its
292
+ module: `ordering.module.ts`. A second one is reported by
293
+ [`layers/no-outward-import`](../rules/layers/no-outward-import.md), and it re-exports nothing:
294
+ see [`strategic/no-cross-context-import`](../rules/strategic/no-cross-context-import.md). It is a class that builds everything with `new`, or the module of your
295
+ framework's container, such as a NestJS `@Module`: see [Integrations](../integrations/index.md).
296
+
297
+ ::: tip
298
+ It is the only file that sees every layer, and the only one, besides an anti-corruption layer, that
299
+ may import from another context: another context's module, to reach its open host services.
300
+ :::
301
+
302
+ At the root of `src/`, `main.ts` starts the application and `app.module.ts` builds or imports the
303
+ module of each bounded context. These files import composition roots only.
304
+
305
+ ## Shared kernel
306
+
307
+ `src/shared-kernel/` holds what every bounded context needs in the same form: value objects such
308
+ as `Money`, ports such as a tracer, and their adapters. It has the same layers as a bounded
309
+ context, possibly grouped by feature (`shared-kernel/time/driven/system/adapters/`). Every context
310
+ may import it; it imports none of them.
311
+
312
+ ::: warning Keep it small
313
+ Each change to the shared kernel reaches every context. `Clock` and `IdGenerator` already come
314
+ with `@alveolus/core`; only their adapters live here. An aggregate, a repository or a handler in
315
+ the shared kernel is reported by
316
+ [`strategic/no-fat-shared-kernel`](../rules/strategic/no-fat-shared-kernel.md).
317
+ :::
318
+
319
+ ## See also
320
+
321
+ - [Getting started](./getting-started.md), to configure and run the checks
322
+ - [Building blocks](../core/index.md), the classes each folder holds
323
+ - [Integrations](../integrations/index.md), to write the composition root with your framework
324
+ - Rules: [`layers/no-outward-import`](../rules/layers/no-outward-import.md), [`tactical/no-misplaced-class`](../rules/tactical/no-misplaced-class.md), [`strategic/no-cross-context-import`](../rules/strategic/no-cross-context-import.md), [`layers/no-impure-domain`](../rules/layers/no-impure-domain.md), [`tactical/no-loose-code`](../rules/tactical/no-loose-code.md), [`layers/no-portless-adapter`](../rules/layers/no-portless-adapter.md)
@@ -0,0 +1,42 @@
1
+ ---
2
+ description: "What a version number of @alveolus/core and @alveolus/arch promises: what changes in a patch, a minor and a major, and how to update."
3
+ ---
4
+
5
+ # Versioning
6
+
7
+ Both packages follow [semantic versioning](https://semver.org/), with the same version number,
8
+ released together. The changelog of each package says what changed and why; read it before you
9
+ update, the way you would read the notes of a linter.
10
+
11
+ ## Before 1.0
12
+
13
+ A `0.x` version can change between minors: a rule renamed, a configuration key moved, a message
14
+ reworded. Each change is in the changelog, with what to do. `1.0.0` comes when a project has
15
+ lived on the checks long enough to trust them, on Linux, macOS and Windows.
16
+
17
+ ## From 1.0
18
+
19
+ | Change | Version |
20
+ | --- | --- |
21
+ | A bug fixed, a message reworded, a page corrected | patch |
22
+ | A new rule, **reported as an error from its first version** | minor |
23
+ | A new configuration key, a new output format, a new option | minor |
24
+ | A rule that reports more than before, on code it accepted | minor, said in the changelog |
25
+ | A rule renamed or removed, a configuration key renamed or removed | major |
26
+ | A building block whose signature changes, a type removed from `@alveolus/core` | major |
27
+ | A higher Node.js version required | major |
28
+
29
+ A rule keeps its id for as long as it exists: there is no alias. When an id has to change, it
30
+ changes at a major, and the changelog gives the old and the new name.
31
+
32
+ ## Update
33
+
34
+ A minor can add a rule that fails your check: that is the point of a rule. Read the changelog,
35
+ run `npx alveolus arch check`, and either fix what it reports, lower the rule to `warn` for a
36
+ while, or record it in the baseline. A major comes with migration notes in its changelog.
37
+
38
+ ## See also
39
+
40
+ - [Getting started](./getting-started.md), the configuration and the levels of the rules
41
+ - [The changelog of `@alveolus/arch`](https://github.com/alveolusjs/alveolus/blob/main/packages/arch/CHANGELOG.md)
42
+ and [of `@alveolus/core`](https://github.com/alveolusjs/alveolus/blob/main/packages/core/CHANGELOG.md)
@@ -0,0 +1,112 @@
1
+ ---
2
+ description: "Use Alveolus with any framework: only the composition root of each bounded context knows how its classes are built, by hand or with a DI container."
3
+ ---
4
+
5
+ # Integrations
6
+
7
+ Alveolus imposes no framework: only the composition root of each bounded context knows how its
8
+ classes are built, by hand or with a container.
9
+
10
+ <dl class="al-glance">
11
+ <dt>Needs</dt><dd>No bus, no container, no ORM, no decorator</dd>
12
+ <dt>Wired in</dt><dd>The <a href="/guide/project-layout#composition-root">composition root</a>, <code>ordering.module.ts</code></dd>
13
+ <dt>Tokens</dt><dd>The abstract classes of ports and repositories</dd>
14
+ <dt>Fits</dt><dd>Express, Fastify, Hono, plain Node.js, <a href="./nestjs">NestJS</a></dd>
15
+ <dt>Checked by</dt><dd><a href="/rules/layers/no-outward-import"><code>layers/no-outward-import</code></a>, <a href="/rules/layers/no-impure-domain"><code>layers/no-impure-domain</code></a></dd>
16
+ </dl>
17
+
18
+ ## Why
19
+
20
+ A framework changes more often than the business rules. When a handler carries its decorators,
21
+ reads the request or imports the database client, moving to another framework, or just upgrading
22
+ it, means touching the use cases, and their tests need the framework to run.
23
+
24
+ ::: tip The fix
25
+ The domain and the application import no framework. Every class receives its dependencies in its
26
+ constructor, typed with abstract classes. The framework stays at the edge: in the adapters, and in
27
+ the one file that builds everything.
28
+ :::
29
+
30
+ ## How it works
31
+
32
+ The composition root builds the adapters and passes them to the handlers, which only know the
33
+ abstract classes they extend. Controllers, consumers and jobs then call the handlers.
34
+
35
+ <div class="al-diagram">
36
+ <svg viewBox="0 0 680 220" role="img" aria-label="The composition root ordering.module.ts builds the adapters PgOrders, SystemClock and RandomIdGenerator, and passes them to the constructor of PlaceOrderHandler, which knows them only as Orders, Clock and IdGenerator.">
37
+ <defs>
38
+ <marker id="wiring-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
39
+ <path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
40
+ </marker>
41
+ </defs>
42
+ <rect class="boundary" x="8" y="82" width="170" height="56" rx="8" />
43
+ <text class="label" x="93" y="106" text-anchor="middle">ordering.module.ts</text>
44
+ <text class="note" x="93" y="126" text-anchor="middle">composition root</text>
45
+ <rect class="box" x="250" y="16" width="190" height="56" rx="8" />
46
+ <text class="label" x="345" y="40" text-anchor="middle">PgOrders</text>
47
+ <text class="note" x="345" y="60" text-anchor="middle">extends Orders</text>
48
+ <rect class="box" x="250" y="82" width="190" height="56" rx="8" />
49
+ <text class="label" x="345" y="106" text-anchor="middle">SystemClock</text>
50
+ <text class="note" x="345" y="126" text-anchor="middle">extends Clock</text>
51
+ <rect class="box" x="250" y="148" width="190" height="56" rx="8" />
52
+ <text class="label" x="345" y="172" text-anchor="middle">RandomIdGenerator</text>
53
+ <text class="note" x="345" y="192" text-anchor="middle">extends IdGenerator</text>
54
+ <rect class="box" x="500" y="82" width="172" height="56" rx="8" />
55
+ <text class="label" x="586" y="106" text-anchor="middle">PlaceOrderHandler</text>
56
+ <text class="note" x="586" y="126" text-anchor="middle">knows the abstractions</text>
57
+ <path class="link" d="M 178 110 L 248 44" marker-end="url(#wiring-arrow)" />
58
+ <path class="link" d="M 178 110 L 248 110" marker-end="url(#wiring-arrow)" />
59
+ <path class="link" d="M 178 110 L 248 176" marker-end="url(#wiring-arrow)" />
60
+ <text class="note" x="213" y="100" text-anchor="middle">new</text>
61
+ <path class="link" d="M 440 44 L 498 106" marker-end="url(#wiring-arrow)" />
62
+ <path class="link" d="M 440 110 L 498 110" marker-end="url(#wiring-arrow)" />
63
+ <path class="link" d="M 440 176 L 498 114" marker-end="url(#wiring-arrow)" />
64
+ </svg>
65
+ </div>
66
+
67
+ There are two ways to write the composition root:
68
+
69
+ <div class="al-cards al-cards-2">
70
+ <div class="al-card"><span class="al-card-title"><a href="#without-a-container">Without a container</a></span>With Express, Fastify, Hono or plain Node.js: a class builds everything with <code>new</code>.</div>
71
+ <div class="al-card"><span class="al-card-title"><a href="#with-a-container">With a container</a></span>With NestJS or another container: each adapter is registered under the abstract class it extends.</div>
72
+ </div>
73
+
74
+ ## Without a container
75
+
76
+ The composition root is a plain class. Its constructor receives what comes from outside, such as
77
+ the database pool, and builds each handler with its adapters.
78
+
79
+ ```ts [src/ordering/ordering.module.ts]
80
+ export class OrderingModule {
81
+ readonly placeOrder: PlaceOrderHandler;
82
+
83
+ constructor(db: Pool) {
84
+ this.placeOrder = new PlaceOrderHandler(
85
+ new PgOrders(db),
86
+ new SystemClock(),
87
+ new RandomIdGenerator(),
88
+ );
89
+ }
90
+ }
91
+ ```
92
+
93
+ Routes, consumers and jobs then call `handle` and turn the `Result` into a response: see
94
+ [Command handlers](../core/application/command-handlers.md).
95
+
96
+ ## With a container
97
+
98
+ The abstract classes of your ports and repositories are the injection tokens: register each
99
+ adapter under the class it extends, and the container passes it to every handler that asks for it.
100
+ No token constant, no string key.
101
+
102
+ ::: tip
103
+ The [NestJS](./nestjs.md) guide shows the providers, the choice between factories and
104
+ `@Injectable()`, and how two modules connect.
105
+ :::
106
+
107
+ ## See also
108
+
109
+ - [NestJS](./nestjs.md), the composition root as a NestJS module
110
+ - [Project layout](../guide/project-layout.md#composition-root), where the composition root lives
111
+ - [Command handlers](../core/application/command-handlers.md), the classes it builds
112
+ - Rules: [`layers/no-outward-import`](../rules/layers/no-outward-import.md), [`layers/no-impure-domain`](../rules/layers/no-impure-domain.md)
@@ -0,0 +1,169 @@
1
+ ---
2
+ description: "Domain-Driven Design with NestJS: each bounded context is a NestJS module, and the abstract classes of ports and repositories are its injection tokens."
3
+ ---
4
+
5
+ # NestJS
6
+
7
+ Each bounded context is a NestJS module, and the abstract classes of ports and repositories are its
8
+ injection tokens.
9
+
10
+ <dl class="al-glance">
11
+ <dt>Bounded context</dt><dd>One <code>@Module</code>, in <code>ordering.module.ts</code></dd>
12
+ <dt>Tokens</dt><dd>The abstract classes of ports and repositories</dd>
13
+ <dt>Handlers</dt><dd><a href="#register-the-handlers">A factory provider</a>, or <a href="#or-use-injectable-in-the-application"><code>@Injectable()</code></a> if you allow it</dd>
14
+ <dt>Exports</dt><dd><a href="#connect-two-bounded-contexts">Open host services</a> only</dd>
15
+ <dt>Checked by</dt><dd><a href="/rules/layers/no-outward-import"><code>layers/no-outward-import</code></a>, <a href="/rules/strategic/no-cross-context-import"><code>strategic/no-cross-context-import</code></a></dd>
16
+ </dl>
17
+
18
+ ## Why
19
+
20
+ NestJS invites a decorator on every class. On a handler, `@Injectable()` makes the application
21
+ import `@nestjs/common`: the use cases then depend on the framework, and the build needs
22
+ `emitDecoratorMetadata`.
23
+
24
+ ::: tip The fix
25
+ Keep NestJS in the module and the adapters. The module registers each adapter under its abstract
26
+ class and builds each handler with a factory. The application imports nothing from NestJS.
27
+ :::
28
+
29
+ ## Design it
30
+
31
+ ### Choose how to register the handlers
32
+
33
+ | If you want… | Then register handlers… |
34
+ | --- | --- |
35
+ | an application free of NestJS, built by any tool | with a [**factory**](#register-the-handlers): the default |
36
+ | shorter modules, and accept the framework in the application | as [**`@Injectable()` classes**](#or-use-injectable-in-the-application) |
37
+
38
+ Adapters are outside the application: they may carry `@Injectable()` either way.
39
+
40
+ ## Usage
41
+
42
+ ### Register the handlers
43
+
44
+ A handler has no `@Injectable()`, so NestJS cannot read its constructor: register it with a
45
+ factory, listing its constructor tokens in order.
46
+
47
+ ```ts [src/ordering/ordering.module.ts]
48
+ providers: [
49
+ { provide: Orders, useClass: PgOrders },
50
+ { provide: Clock, useClass: SystemClock },
51
+ { provide: IdGenerator, useClass: RandomIdGenerator },
52
+ {
53
+ inject: [Orders, Clock, IdGenerator],
54
+ provide: PlaceOrderHandler,
55
+ useFactory: (orders: Orders, clock: Clock, ids: IdGenerator) =>
56
+ new PlaceOrderHandler(orders, clock, ids),
57
+ },
58
+ ],
59
+ ```
60
+
61
+ `PgOrders` is a driven adapter: it may carry `@Injectable()` to receive the database client.
62
+
63
+ ### Or use `@Injectable()` in the application
64
+
65
+ To list handlers as plain providers, allow `Injectable`, and only it, in the application:
66
+
67
+ ```ts [alveolus.config.ts]
68
+ applicationDependencies: { "@nestjs/common": ["Injectable"] },
69
+ ```
70
+
71
+ ```ts [src/ordering/application/commands/place-order.command.ts]
72
+ @Injectable()
73
+ export class PlaceOrderHandler extends CommandHandler<
74
+ PlaceOrder,
75
+ void,
76
+ PlaceOrderError
77
+ > { … }
78
+ ```
79
+
80
+ ```ts [src/ordering/ordering.module.ts]
81
+ providers: [{ provide: Orders, useClass: PgOrders }, PlaceOrderHandler],
82
+ ```
83
+
84
+ ::: warning Caveats
85
+ - The application then depends on NestJS, and needs `emitDecoratorMetadata`: Vitest and other
86
+ esbuild-based tools need a SWC transform.
87
+ - Import the injected classes as values, not with `import type`: the metadata needs them at
88
+ runtime.
89
+ :::
90
+
91
+ ### Connect two bounded contexts
92
+
93
+ So that nothing else of the catalog leaks out, a module exports its
94
+ [open host services](../core/strategic/open-host-services.md), and nothing else. The downstream
95
+ module imports it to build its [anti-corruption layer](../core/strategic/anti-corruption-layers.md).
96
+
97
+ ```ts [src/catalog/catalog.module.ts]
98
+ @Module({
99
+ exports: [CatalogApi],
100
+ providers: [CatalogApi, …],
101
+ })
102
+ export class CatalogModule {}
103
+ ```
104
+
105
+ ```ts [src/ordering/ordering.module.ts]
106
+ @Module({
107
+ imports: [CatalogModule],
108
+ providers: [{ provide: PriceList, useClass: CatalogPriceList }, …],
109
+ })
110
+ export class OrderingModule {}
111
+ ```
112
+
113
+ Exporting a repository or a handler would let another context reach the model behind the open host
114
+ service. Checked by [`strategic/no-cross-context-import`](../rules/strategic/no-cross-context-import.md).
115
+
116
+ ### Answer with a `Result`
117
+
118
+ So that a controller stays a translation, it calls the handler, and maps the `Result` to a
119
+ response: a success to the status code, a failure to the HTTP error the client can act on. The
120
+ mapping lives once, in an exception filter or a small mapper class of `driving/http/`.
121
+
122
+ ```ts [src/ordering/driving/http/orders.controller.ts]
123
+ @Controller("orders")
124
+ export class OrdersController {
125
+ constructor(private readonly placeOrder: PlaceOrderHandler) {}
126
+
127
+ @Post(":id/place")
128
+ async place(@Param("id") id: string): Promise<void> {
129
+ const result = await this.placeOrder.handle({ orderId: id });
130
+ if (!result.ok) {
131
+ throw new HttpFailure(result.error);
132
+ }
133
+ }
134
+ }
135
+ ```
136
+
137
+ `HttpFailure` is a class of `driving/http/` that turns a `DomainError` into an `HttpException`
138
+ by its `type`: `EmptyOrder` to `422`, `OrderNotFound` to `404`. The domain never knows HTTP.
139
+
140
+ ### Run the check with your lint
141
+
142
+ So that a violation is seen before the review, the check runs where the lint runs:
143
+
144
+ ```json [package.json]
145
+ {
146
+ "scripts": {
147
+ "lint": "biome check . && alveolus arch check",
148
+ "lint:arch": "alveolus arch check --format sarif > arch.sarif"
149
+ }
150
+ }
151
+ ```
152
+
153
+ In CI, `alveolus arch check` fails the job on an error; `--format sarif` and
154
+ `github/codeql-action/upload-sarif` put each violation on the line it concerns in the pull
155
+ request.
156
+
157
+ ## Troubleshooting
158
+
159
+ **`Nest can't resolve dependencies of PlaceOrderHandler (?, …)`**: the handler is listed as a plain
160
+ class without `@Injectable()`, an injected class is imported with `import type`, or a token of
161
+ `inject` has no provider.
162
+
163
+ ## See also
164
+
165
+ - [Integrations](./index.md), the composition root without a framework
166
+ - [Command handlers](../core/application/command-handlers.md), the classes the module builds
167
+ - [Open host services](../core/strategic/open-host-services.md) and
168
+ [Anti-corruption layers](../core/strategic/anti-corruption-layers.md), to connect two modules
169
+ - Rules: [`layers/no-outward-import`](../rules/layers/no-outward-import.md), [`strategic/no-cross-context-import`](../rules/strategic/no-cross-context-import.md)