Build1 publisher3 min readPublished
The NestJS default path puts the query inside the business rule, and nothing fails when it moves
A dev.to teardown of the framework-documented TypeORM setup counts five pieces of persistence knowledge inside one order-confirmation method. Four are annoyances. One is structural.
The Engineer · Build desk
Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction
What happened
- The canonical starting point for a NestJS module backed by TypeORM is the one the framework documents: the module declares TypeOrmModule.forFeature([Order]) and the service receives the repository by injection with @InjectRepository(Order).
- From there the service has find, findOne, save and delete at hand and can write its first business rule with no further scaffolding; it is the path of least friction and the best documented, which is why most codebases are built on it.
- The example service method confirm(orderId) calls repo.findOne with where { id: orderId, status: 'pending' } and relations { lines: true }, throws NotFoundException if the result is null, throws BadRequestException('Cannot confirm an order without lines') if order.lines.length === 0, sets order.status = 'confirmed', and returns this.repo.save(order).
- The author states the method is correct: it does what it promises, it reads well, and it can carry years of production without an incident; what is worth analysing is its coupling surface, not its behaviour.
- The genuine business rule inside the method is that an order cannot be confirmed with no lines.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
A post on dev.to walks through the NestJS module the framework itself documents, `TypeOrmModule.forFeature([Order])` with the repository injected into the service, and then inventories what that arrangement costs inside a single method [1][2]. Its count is five distinct pieces of persistence knowledge embedded in one business rule, and the argument that makes it worth reading is that the method reads perfectly well anyway [3][4].
The example is an order confirmation: `findOne` with `where { id, status: 'pending' }` and `relations { lines: true }`, a null check, a check that the order has lines, a status assignment, `save` [3]. The genuine rule in there is that an order cannot be confirmed with no lines [5]. The rest is the ORM.
Start with the failure mode, because it is the one that gets paged. The guard is evaluated over `order.lines`, and that array is only populated because the query asked for the relation [6]. According to the post, drop `relations: { lines: true }` in a refactor and the collection arrives empty, the guard fires on orders that do have lines, and the invariant is inverted with no compile error, no exception and no trace [6]. That is zero of the three channels a team normally relies on to notice a regression [14]. The correctness of the rule becomes a property of the query rather than of the rule [6].
The other local couplings are cheaper but same family. `'pending'` is a string compared against a column, not a domain concept [7]. The first `if` exists because `findOne` returns `null`, which is TypeORM's decision and not the business's [8]. `save` resolves INSERT versus UPDATE from the state of the primary key, and the service inherits that ambiguity [9].
The fifth is different in kind. `Order`, where the total calculation and the state transitions will eventually live, is the same class carrying the `@Column` decorators that describe the table [10]. Four of the five items are local and reversible; one is not [11][15]. The post's framing is that this one was a structural decision the project adopted without deliberating it, by following the documented path [11].
The entity listing shows both directions of the leak. `total` is declared `numeric` with precision 12 and scale 2 and typed as `string` in TypeScript, sitting beside `createdAt`, `updatedAt` and `deletedAt`, which the author classifies as persistence arriving in the domain [12][13]. One class serves two consumers with incompatible requirements: the business wants an object that can only exist in valid states, the ORM wants an object it can build knowing nothing about the business [16].
The timing claim is the operationally useful part. The coupling has no observable cost while the module stays on single-entity operations over a single table [17]. It becomes measurable when three things show up: the entity accumulates invariants of its own, a query with business meaning is needed from more than one place, and a rule has to be verified without a database [18]. The author's position is that almost every real domain ends up meeting all three [18]. Which puts the boundary in an awkward spot commercially: it is cheapest to install while the service method still reads fine, and the reason nobody installs it then is that nothing looks wrong yet.
What to watch in your own repository is the `relations` argument. The number worth knowing is how many business invariants depend on a field list set at the call site [6]. This is a single practitioner post, not a benchmark, and the three costs it promises to examine separately are where the case actually has to be made [18].