<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[NestJS Skills]]></title><description><![CDATA[Practical NestJS guides and open-source AI coding skills for backend architecture, code reviews, performance, testing, and maintainable TypeScript applications.]]></description><link>https://nestjs-skills.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ac3375e15779f8260ce464b/ed48ed8b-4bdf-436f-ac72-55e7a9a80c25.png</url><title>NestJS Skills</title><link>https://nestjs-skills.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 13 Oct 2026 15:39:05 GMT</lastBuildDate><atom:link href="https://nestjs-skills.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[NestJS hexagonal architecture: where DTOs, domain models, and types belong]]></title><description><![CDATA[Start with the boundary: who sends this data, who owns its meaning, and who is allowed to change it?
In a NestJS hexagonal architecture, use transport DTOs for HTTP or message contracts, application c]]></description><link>https://nestjs-skills.hashnode.dev/nestjs-hexagonal-architecture-where-dtos-domain-models-and-types-belong</link><guid isPermaLink="true">https://nestjs-skills.hashnode.dev/nestjs-hexagonal-architecture-where-dtos-domain-models-and-types-belong</guid><dc:creator><![CDATA[amir taherkhani]]></dc:creator><pubDate>Sun, 11 Oct 2026 18:54:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ac3375e15779f8260ce464b/0d76637a-e09d-483a-a126-345904b59de4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Start with the boundary:</strong> who sends this data, who owns its meaning, and who is allowed to change it?</p>
<p>In a NestJS hexagonal architecture, use transport DTOs for HTTP or message contracts, application commands and results for use cases, and domain entities or value objects where business identity and rules matter. Keep database representations inside persistence adapters.</p>
<blockquote>
<p><strong>You do not need a new class for every function call.</strong></p>
</blockquote>
<p>The <a href="https://amirtaherkhani.github.io/nestjs-skills/">NestJS Skills documentation</a> covers the broader architecture and object-design guidance. The <a href="https://github.com/amirtaherkhani/nestjs-skills">repository</a> includes the rules and examples behind it.</p>
<blockquote>
<p><strong>Version context:</strong> this example targets NestJS 11.2.7, TypeScript 5.9.3, class-validator 0.15.1, and class-transformer 0.5.1.</p>
</blockquote>
<p>The Nest links use the v11 documentation because the default documentation may describe a newer major version. Follow your project's conventions first, while checking that its validation and deطpendency-injection setup actually works at runtime.</p>
<h2>Find the boundary before choosing the type</h2>
<p><a href="https://alistair.cockburn.us/hexagonal-architecture">Hexagonal architecture</a>, also called ports and adapters, separates the business core from the technologies around it. The core should be understandable and testable without starting an HTTP server or connecting to a database.</p>
<p>Inside that core, the domain owns business meaning and rules. An application use case coordinates one goal, such as logging in: load the needed state, ask the domain to apply its rules, and call the required external capabilities. Separating these roles does not require a large class hierarchy.</p>
<p>Ports define the contracts for those interactions. An incoming port exposes an operation such as <code>Login.execute</code>. An outgoing port describes a capability the application needs, such as <code>UserRepository.findByEmail</code>. The contract describes expected results and failures as well as the method signature. You do not need an interface for every helper.</p>
<p>Adapters connect those contracts to technology:</p>
<ol>
<li><p>An incoming adapter, such as a Nest HTTP controller, receives a request, validates its transport data, and invokes the application operation.</p>
</li>
<li><p>The application coordinates domain behavior and calls outgoing ports when it needs stored data or another external capability.</p>
</li>
<li><p>An outgoing adapter, such as a SQL repository, implements a port and translates persistence records into the promised result.</p>
</li>
<li><p>The incoming adapter maps the application result back to an HTTP response.</p>
</li>
</ol>
<p>Source dependencies point inward: adapters depend on the application's contracts, and the application can depend on the domain. The domain does not import the HTTP controller or SQL adapter. A runtime call can still travel outward to the database through an injected port implementation.</p>
<p>For the User/Auth example below, an HTTP request reaches the login use case, a repository adapter loads the user, the domain checks account state, and the controller returns a mapped result. A test can call that same use case with an in-memory repository. Another adapter can later implement the same port if it preserves the contract's behavior.</p>
<p>This approach helps when an application has substantial business rules or needs to stay maintainable through years of changes. It also helps when several incoming adapters need the same operations or when you may replace an external provider. For example, an order-payment use case can serve both an HTTP checkout and a queue consumer while calling a replaceable payment-provider adapter. A small CRUD service with few rules and one stable data source may not justify the extra abstractions.</p>
<p>Isolated tests and adapter replacement become easier, but you have more contracts, wiring, and mapping to maintain. Add those abstractions where they protect a real boundary.</p>
<p>The hexagon does not mean six layers, six modules, or one mandatory folder layout. A <code>Dto</code> suffix or a folder named <code>domain</code> does not establish the architecture; the imports, data flow, and ownership do.</p>
<h2>Give each data shape a clear job</h2>
<p>A DTO carries data between parts of a system. It can describe an HTTP request, an API response, a queue message, or the input and result of an application operation. It does not have to be a decorated class.</p>
<p>An entity has identity and continuity across changes. A value object describes a value and its rules, such as money with a currency or a valid date range. A domain model also includes business behavior and policies. Naming a TypeScript interface <code>User</code> does not, by itself, give it those rules.</p>
<p>An application command such as <code>LoginCommand</code> describes a use case's input. It can be a plain type. Calling it an application DTO is also reasonable; ownership and behavior matter more than the suffix.</p>
<table>
<thead>
<tr>
<th>Location or boundary</th>
<th>Useful default</th>
<th>Reason to change it</th>
</tr>
</thead>
<tbody><tr>
<td><strong>HTTP, RPC, or incoming message</strong></td>
<td>Transport DTO plus runtime validation</td>
<td>Use a schema and custom pipe if that fits the project</td>
</tr>
<tr>
<td><strong>Application use case</strong></td>
<td>Command/result types or small classes</td>
<td>Use a class when a runtime constructor or behavior has a purpose</td>
</tr>
<tr>
<td><strong>Same-domain collaboration</strong></td>
<td>Entities, value objects, or focused values</td>
<td>Return a projection when the caller only needs a few fields</td>
</tr>
<tr>
<td><strong>Another module or external system</strong></td>
<td>Explicit public contract or message DTO</td>
<td>Share domain values only when ownership and meaning are deliberately shared</td>
</tr>
<tr>
<td><strong>Repository adapter</strong></td>
<td>Domain-facing port, persistence-facing records</td>
<td>A simple CRUD feature may not benefit from a separate rich domain model</td>
</tr>
</tbody></table>
<p>Two Nest modules can still belong to one domain boundary. A folder split alone does not require a DTO conversion. Across independently owned capabilities, however, importing another module's internal entity can make an unrelated change break your code.</p>
<h2>Type, interface, class, or schema?</h2>
<p>A TypeScript <code>type</code> or <code>interface</code> works well for an internal contract. It helps the compiler check callers. It disappears from the JavaScript output, so it cannot inspect JSON at runtime.</p>
<p>An ordinary class has a runtime constructor. It can hold behavior or provide a Nest injection token. Its declared field types still do not validate incoming values.</p>
<p>A decorated DTO class supports the familiar <code>ValidationPipe</code> plus <code>class-validator</code> path. A runtime schema is another option: a custom pipe can parse an <code>unknown</code> value and return a typed result. In either approach, the runtime check is the part that establishes trust. <code>JSON.parse(...) as User</code> only tells the compiler what to assume.</p>
<p>Nest's <a href="https://docs.nestjs.com/v11/techniques/validation">v11 validation guide</a> explains why concrete classes matter for reflected DTO validation. Import a DTO class as a runtime value when Nest needs its metadata. A type-only import cannot provide that constructor.</p>
<p>A plain object is a runtime value and can carry an application command or a mapped response. A <code>type</code> or <code>interface</code> can describe its shape. A normal TypeScript <code>enum</code> also has a runtime representation, but annotating a field with it does not validate JSON. Use a shared enum or string union only when the layers intentionally share the same meanings; map transport values when they differ.</p>
<p>Choose the representation your contract needs. An empty class for every internal parameter adds files without automatically adding protection.</p>
<h2>Follow a login request across the boundaries</h2>
<p>This example treats User and Auth as parts of the same identity domain. The application can therefore use the domain <code>User</code> directly. If they were independently owned capabilities, Auth would call a narrow public authentication contract instead of importing Users' internal entity or password hash.</p>
<p>The request first passes a transport validator. The application then loads a user through a repository port, checks the credentials, applies the account-state rule, and requests a session. The controller maps the application result to the response.</p>
<h3>1. Let the domain own account rules</h3>
<p>The domain has no Nest or ORM imports. A disabled account cannot log in, even if its password is correct. The application supplies the password-verification function, so the entity does not import a hashing library.</p>
<pre><code class="language-typescript">export class LoginRejected extends Error {}

// Identity and account-state rules belong to this domain model.
export class User {
  constructor(
    readonly id: string,
    readonly email: string,
    private readonly active: boolean,
    private readonly passwordHash: string,
  ) {}

  assertCanLogin(): void {
    if (!this.active) throw new LoginRejected();
  }

  matchesPassword(
    plain: string,
    verify: (plain: string, hash: string) =&gt; Promise&lt;boolean&gt;,
  ): Promise&lt;boolean&gt; {
    return verify(plain, this.passwordHash);
  }
}
</code></pre>
<p>For a larger identity model, construction and state changes would protect more invariants. This small example assumes the adapter loads a valid persisted user. A richer reconstitution factory can reject invalid stored states when that is a real requirement.</p>
<h3>2. Define the application contract</h3>
<p>The use case depends on capabilities, not HTTP or SQL details. It returns the information its caller needs, with no domain entity in the result.</p>
<pre><code class="language-typescript">import { LoginRejected, User } from './domain';

export interface UserRepository {
  findByEmail(email: string): Promise&lt;User | null&gt;;
}
export interface PasswordVerifier {
  verify(plain: string, hash: string): Promise&lt;boolean&gt;;
}
export interface SessionIssuer {
  issue(userId: string): Promise&lt;string&gt;;
}
export type LoginCommand = Readonly&lt;{ email: string; password: string }&gt;;
export type LoginResult = Readonly&lt;{
  accessToken: string;
  user: Readonly&lt;{ id: string; email: string }&gt;;
}&gt;;

export class Login {
  constructor(
    private readonly users: UserRepository,
    private readonly passwords: PasswordVerifier,
    private readonly sessions: SessionIssuer,
  ) {}

  async execute(command: LoginCommand): Promise&lt;LoginResult&gt; {
    const user = await this.users.findByEmail(command.email);
    if (!user) throw new LoginRejected();
    const valid = await user.matchesPassword(
      command.password,
      (plain, hash) =&gt; this.passwords.verify(plain, hash),
    );
    if (!valid) throw new LoginRejected();
    user.assertCanLogin();
    return {
      accessToken: await this.sessions.issue(user.id),
      user: { id: user.id, email: user.email },
    };
  }
}
</code></pre>
<p>The input shape currently matches the request DTO. The separate name makes the application contract explicit, without another class.</p>
<p>TypeScript is structurally typed, so these names alone do not enforce isolation. Import rules and tests can help keep transport dependencies out of the application.</p>
<p>The plain <code>Login</code> class can be registered with a Nest <code>useFactory</code> provider. Wire its repository, verifier, and session adapters there. Interfaces do not work as runtime injection tokens; use explicit tokens where needed. Nest's <a href="https://docs.nestjs.com/v11/fundamentals/custom-providers">custom provider documentation</a> covers these provider forms.</p>
<h3>3. Keep database details in the adapter</h3>
<p><code>UserRow</code> stands in for the record an ORM returns. Its column names and status representation belong to persistence. The repository port promises a domain <code>User</code>.</p>
<pre><code class="language-typescript">import { UserRepository } from './application';
import { User } from './domain';

// Structural stand-in for the persistence record returned by an ORM.
export type UserRow = {
  id: string;
  email: string;
  status: 'active' | 'disabled';
  password_hash: string;
};
export interface UserTable {
  findByEmail(email: string): Promise&lt;UserRow | null&gt;;
}
export class SqlUserRepository implements UserRepository {
  constructor(private readonly table: UserTable) {}

  async findByEmail(email: string): Promise&lt;User | null&gt; {
    const row = await this.table.findByEmail(email);
    return row
      ? new User(row.id, row.email, row.status === 'active', row.password_hash)
      : null;
  }
}
</code></pre>
<blockquote>
<p><strong>An ORM entity and a domain entity have different jobs.</strong></p>
</blockquote>
<p>ORM decorators, lazy relations, database defaults, and change tracking should not become hidden requirements of a framework-free domain model.</p>
<p>For read-only reporting, a query port can return a purpose-built projection instead of rebuilding a full entity. For a small CRUD feature with no meaningful domain behavior, a separate domain class may not be worth maintaining. That still leaves you responsible for controlling the public response.</p>
<h3>4. Validate what comes in, map what goes out</h3>
<p>The request is a decorated class because this route uses Nest's class-based validation. The response is a type because this example constructs its JSON explicitly and does not use reflection-based response serialization.</p>
<pre><code class="language-typescript">import 'reflect-metadata';
import { Body, Controller, HttpCode, Post, UnauthorizedException, ValidationPipe } from '@nestjs/common';
import { IsEmail, IsString, MaxLength, MinLength } from 'class-validator';
import { Login, LoginResult } from './application';
import { LoginRejected } from './domain';

export class LoginRequestDto {
  @IsEmail()
  @MaxLength(254)
  email!: string;

  @IsString()
  @MinLength(1)
  @MaxLength(128)
  password!: string;
}
export const inputPipe = new ValidationPipe({
  transform: true,
  whitelist: true,
  forbidNonWhitelisted: true,
});
export type LoginResponseDto = {
  access_token: string;
  user: { id: string; email: string };
};
export function toResponse(result: LoginResult): LoginResponseDto {
  return {
    access_token: result.accessToken,
    user: { id: result.user.id, email: result.user.email },
  };
}
@Controller('auth')
export class AuthController {
  constructor(private readonly login: Login) {}

  @Post('login')
  @HttpCode(200)
  async execute(@Body(inputPipe) body: LoginRequestDto): Promise&lt;LoginResponseDto&gt; {
    try {
      const result = await this.login.execute({
        email: body.email,
        password: body.password,
      });
      return toResponse(result);
    } catch (error) {
      if (error instanceof LoginRejected) throw new UnauthorizedException();
      throw error;
    }
  }
}
</code></pre>
<p>Here, <code>transform</code> returns a DTO instance, <code>whitelist</code> enables checking against validation metadata, and <code>forbidNonWhitelisted</code> rejects extra properties. These options do not replace business rules or authorization. The example's length limits are illustrative input limits; use limits compatible with your real account and password policies.</p>
<p>The mapper names every output field, including the nested user fields. Returning <code>{ ...user }</code> or relying on a return-type annotation would not remove extra properties at runtime.</p>
<p>TypeScript's <code>private</code> keyword also does not make an emitted property safe to serialize. Use an explicit projection or a correctly configured serialization strategy, then test the actual output.</p>
<p><code>LoginRejected</code> becomes an HTTP 401 at the transport boundary. The application itself has no HTTP exception dependency. Unexpected errors still propagate for normal error handling and logging.</p>
<blockquote>
<p><strong>This is a boundary example, not a complete authentication implementation.</strong></p>
</blockquote>
<p>The runnable tests use fake password, session, and database adapters.</p>
<p>Production authentication also needs a real password verifier and session policy, abuse controls, and review of account-enumeration and timing behavior. The early missing-user return shown here does not equalize password-verification timing. <strong>Never log the command's password or the returned token.</strong></p>
<h2>Make nested DTOs explicit</h2>
<p>A nested TypeScript annotation does not tell a runtime validator how to build or inspect the child object. With this class-based setup, declare the nested validation and transformation together:</p>
<pre><code class="language-typescript">import 'reflect-metadata';
import { Type } from 'class-transformer';
import { IsDefined, IsString, MinLength, ValidateNested } from 'class-validator';

export class AddressDto {
  @IsString()
  @MinLength(1)
  city!: string;
}
export class ProfileRequestDto {
  @IsDefined()
  @ValidateNested()
  @Type(() =&gt; AddressDto)
  address!: AddressDto;
}
export const toProfileCommand = (dto: ProfileRequestDto) =&gt; ({
  address: { city: dto.address.city },
});
</code></pre>
<p><code>@IsDefined()</code> makes the child required. <code>@ValidateNested()</code> checks it, and <code>@Type(() =&gt; AddressDto)</code> identifies the nested class for transformation. The <a href="https://github.com/typestack/class-validator#validating-nested-objects">class-validator</a> and <a href="https://github.com/typestack/class-transformer#working-with-nested-objects">class-transformer</a> documentation describe those separate responsibilities.</p>
<p>For an array of addresses, also validate that the value is an array and validate each child. When mapping, map each element explicitly if it has a different representation or ownership. A shallow spread leaves nested object references shared.</p>
<p>If an address is a meaningful domain value, the application can construct an <code>Address</code> value object and apply its rules. If it is just a small data shape for this operation, a plain object is enough.</p>
<h2>What does <code>User['name']</code> actually reuse?</h2>
<p>An <a href="https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html">indexed-access type</a> lets a function refer to the type of a property. If <code>User.name</code> is a <code>string</code>, <code>User['name']</code> is also a <code>string</code>. It carries no minimum length, allowed-character rule, or proof that validation ran.</p>
<p>In a separate module, the two function signatures look like this:</p>
<pre><code class="language-typescript">// Separate example module: this User has only a static name shape.
export type User = { name: string };
export function displayName(name: User['name']): User['name'] {
  return name;
}
displayName(''); // Compiles. No length or character rule ran.

export class UserName {
  private readonly brand = 'UserName';
  private constructor(readonly value: string) { Object.freeze(this); }

  static parse(input: unknown): UserName {
    // Example product rule, not a universal rule for people's names.
    if (typeof input !== 'string' || !/^[A-Za-z0-9_]{3,30}$/.test(input)) {
      throw new Error('Invalid user name');
    }
    return new UserName(input);
  }
}
export type DomainUser = { readonly name: UserName };
export function renameInput(name: DomainUser['name']): { name: UserName } {
  return { name };
}
const command = renameInput(UserName.parse('amir_1'));
export const nameResponse = { name: command.name.value };
</code></pre>
<p>The first function accepts an empty string because the static shape permits it. The second accepts a <code>UserName</code> value constructed through a runtime factory. The private member prevents an ordinary object with only a <code>value</code> property from satisfying this class type; freezing protects this value from mutation at runtime.</p>
<p>The example rule is for an account handle; it is intentionally not a rule for real human names.</p>
<p>A branded string plus a runtime parsing factory is another option. The brand gives compile-time separation; the factory enforces the rule.</p>
<p>A type assertion can bypass either design's compile-time protection, so casts and external inputs still deserve review. TypeScript <code>readonly</code> and private constructors are compile-time controls, not a security boundary against arbitrary JavaScript mutation.</p>
<p>Once trusted code has constructed a valid value object, it can pass it between internal operations without repeating the same format validation on every call. Rules depending on current state, such as whether an account is still active, may still need checking at the operation that owns them.</p>
<p>Use central types for genuinely shared domain meaning. Avoid a universal <code>User</code> model that owns HTTP fields, database columns, auth secrets, and every module's preferences. An external payload still starts as unknown data: validate and parse it before treating it as a trusted application or domain value.</p>
<h2>Reuse a domain type when the contract fits</h2>
<p>Reuse a small stable value type when its meaning and representation really match. A public string-union status can be shared if both contracts intentionally use the same values. It still needs runtime validation on untrusted input.</p>
<p>Map when the representations differ. A domain <code>Money</code> containing <code>bigint</code> is not automatically JSON-ready. A date string is not a <code>Date</code>, and a raw string does not become a validated identifier because it has a branded TypeScript annotation. Parse those values at an appropriate boundary.</p>
<p>Avoid turning a domain entity into your public schema with <code>Pick&lt;User, ...&gt;</code> just to save a few declarations. That couples the API to an internal model, and a type projection does not strip fields from an object. Never reuse an ORM entity as a public response merely because its properties happen to match today.</p>
<p>Inside one domain, passing an entity or value object between services is often appropriate. Across a public module boundary, expose the operation and result that the consumer needs. For a queue or external API, define a serializable, versioned contract and validate received data, including vendor responses. DTOs are useful at all of these boundaries.</p>
<h2>Catch these mistakes in review</h2>
<ul>
<li><p>Treating <code>type</code>, <code>interface</code>, a class field annotation, or <code>as SomeDto</code> as a runtime validator.</p>
</li>
<li><p>Passing an HTTP DTO through every layer until transport decorators become application dependencies.</p>
</li>
<li><p>Returning domain or ORM objects directly and assuming a response type hides private fields.</p>
</li>
<li><p>Repeating the same shape in a new class at every call, without a different contract or useful runtime behavior.</p>
</li>
<li><p>Moving account-state rules into request validation, or assuming internal calls always passed through the HTTP pipe.</p>
</li>
</ul>
<p>Validation, transformation, mapping, and database access all have costs. Measure them with realistic payloads and request rates before choosing a design for speed. Neither "DTOs are slow" nor "mapping is free" is a useful default. Also consider change cost: how many callers must change when the API, business rule, or database representation changes?</p>
<h2>Put the boundary checks to work with NestJS Skills</h2>
<p>NestJS Skills already separates <a href="https://github.com/amirtaherkhani/nestjs-skills/blob/9dab474301dcc7d42c56f09f0d66a79ea523b76f/skills/nestjs-oop-design-patterns/references/object-design.md">entities, value objects, application operations, adapters, and DTOs</a>. It also describes consumer-owned ports and behavioral contracts. That guidance already exists. An automated review still needs to establish how the boundaries work in the project it is reading.</p>
<p><a href="https://github.com/amirtaherkhani/nestjs-skills/releases/tag/v2.4.0">NestJS Skills v2.4.0</a> now includes executable examples for module contracts, provider wiring, enhancer execution, and runtime/static type boundaries, with 16 new boundary tests. It also corrects three issues: validating and filtering external JSON in the slow-API examples, allowing structurally compatible adapters for abstract-class DI tokens without requiring inheritance, and clarifying that <code>useGlobalFilters()</code> registers supplied instances regardless of whether DI constructed them.</p>
<p>These are focused examples and guidance updates; custom-generator work remains a separate feasibility decision.</p>
<blockquote>
<p><strong>For your next review, take one endpoint and follow its input to its output.</strong> At each boundary, ask what is trusted, who owns the contract, and which test proves that only the intended data crosses it.</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Strategy Pattern in TypeScript and NestJS: A Delivery Quote Example]]></title><description><![CDATA[The Strategy pattern gives one operation several interchangeable implementations. A caller asks for a delivery quote; a selected pricing policy calculates it. The caller does not need to know the poli]]></description><link>https://nestjs-skills.hashnode.dev/strategy-pattern-in-typescript-and-nestjs-a-delivery-quote-example</link><guid isPermaLink="true">https://nestjs-skills.hashnode.dev/strategy-pattern-in-typescript-and-nestjs-a-delivery-quote-example</guid><category><![CDATA[nestjs]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[design patterns]]></category><dc:creator><![CDATA[amir taherkhani]]></dc:creator><pubDate>Wed, 07 Oct 2026 15:01:34 GMT</pubDate><content:encoded><![CDATA[<p>The Strategy pattern gives one operation several interchangeable implementations. A caller asks for a delivery quote; a selected pricing policy calculates it. The caller does not need to know the policy's formula. This is useful when the operation stays the same but its rules genuinely vary.</p>
<h2>A parcel counter example</h2>
<p>Imagine a parcel counter with standard and express delivery. The clerk weighs a parcel, the customer chooses a service, and the clerk gives a price. Both choices need the same input and produce the same kind of answer, but they use different tariffs. For an illustrative parcel weighing 1,500 grams, standard might charge a base 250 cents plus 120 cents for each started kilogram: 490 cents. Express might charge a base 800 cents plus 200 cents for each started kilogram: 1,200 cents.</p>
<p>The clerk's job is stable: accept a valid weight and return a quote for the chosen service. Each tariff owns its calculation. Changing the express tariff should not require rewriting the standard tariff or the clerk's workflow. These numbers are examples, not carrier prices.</p>
<h2>When Strategy helps in software</h2>
<p>The <a href="https://refactoring.guru/design-patterns/strategy">Strategy pattern</a> names three roles: a shared contract, concrete algorithms, and a context that delegates work through that contract. The selection can happen when the context is created or for each operation. In a checkout, the selected service can come from a validated customer choice.</p>
<p>Use it when you already have multiple ways to perform the same operation and each has meaningful rules of its own. It keeps those rules separate and lets you test each one against the same input contract. You still need one place to select the policy, and every policy must agree on units, valid inputs, and errors. A new policy usually means registering another implementation and updating that selection point. Strategy does not eliminate all conditionals.</p>
<p>For one short, stable formula, a function is clearer. Even two small branches may be easier to maintain than extra classes. Introduce the pattern when the variation makes the existing code harder to change or verify.</p>
<h2>Plain TypeScript: keep the quote contract small</h2>
<p>This example keeps money in integer cents and weight in integer grams. Both policies return a quote through <code>quoteCents</code>. <code>DeliveryQuoter</code> owns the shared weight rule and delegates only the calculation.</p>
<pre><code class="language-typescript">export interface DeliveryRate {
  quoteCents(grams: number): number;
}

export class StandardRate implements DeliveryRate {
  quoteCents(grams: number): number {
    return 250 + Math.ceil(grams / 1_000) * 120;
  }
}

export class ExpressRate implements DeliveryRate {
  quoteCents(grams: number): number {
    return 800 + Math.ceil(grams / 1_000) * 200;
  }
}

export class DeliveryQuoter {
  constructor(private readonly rate: DeliveryRate) {}

  quoteCents(grams: number): number {
    if (!Number.isSafeInteger(grams) || grams &lt; 1 || grams &gt; 30_000) {
      throw new RangeError('Weight must be 1 to 30,000 grams');
    }
    return this.rate.quoteCents(grams);
  }
}

const grams = 1_500;
new DeliveryQuoter(new StandardRate()).quoteCents(grams); // 490
new DeliveryQuoter(new ExpressRate()).quoteCents(grams);  // 1200
</code></pre>
<p>The context receives a policy through its constructor. It can use either policy without changing its own code. The 30 kg limit is part of this example's shared contract; a real delivery product would need its own rate tables, eligibility rules, taxes, and currency policy.</p>
<h2>NestJS: choose a provider for each quote</h2>
<p>In NestJS, register both rate classes as providers and inject them into a service. The service selects one for each call. It does not store a mutable "current rate" on a shared provider, which could mix choices from concurrent requests. This code imports the three classes from the TypeScript example above in <code>delivery-rates.ts</code>.</p>
<pre><code class="language-typescript">import {
  BadRequestException,
  Controller,
  Get,
  Injectable,
  Module,
  ParseIntPipe,
  Query,
} from '@nestjs/common';
import {
  DeliveryQuoter,
  ExpressRate,
  StandardRate,
  type DeliveryRate,
} from './delivery-rates';

@Injectable()
export class DeliveryQuoteService {
  constructor(
    private readonly standard: StandardRate,
    private readonly express: ExpressRate,
  ) {}

  quote(mode: string, grams: number): number {
    const rate: DeliveryRate | undefined = mode === 'standard'
      ? this.standard
      : mode === 'express' ? this.express : undefined;
    if (!rate) throw new BadRequestException('Unknown delivery mode');

    try {
      return new DeliveryQuoter(rate).quoteCents(grams);
    } catch (error) {
      if (error instanceof RangeError) {
        throw new BadRequestException(error.message);
      }
      throw error;
    }
  }
}

@Controller('delivery')
export class DeliveryController {
  constructor(private readonly quotes: DeliveryQuoteService) {}

  @Get('quote')
  quote(
    @Query('mode') mode: string,
    @Query('grams', ParseIntPipe) grams: number,
  ) {
    return {
      currency: 'USD',
      amountCents: this.quotes.quote(mode, grams),
    };
  }
}

@Module({
  controllers: [DeliveryController],
  providers: [StandardRate, ExpressRate, DeliveryQuoteService],
})
export class DeliveryModule {}
</code></pre>
<p>For example, <code>GET /delivery/quote?mode=express&amp;grams=1500</code> returns <code>{"currency":"USD","amountCents":1200}</code>. An unknown mode or a weight outside the allowed range receives a 400 response. Test both policies at 1 gram, 1,000 grams, and 1,001 grams, as well as unsupported modes and invalid weights. Those boundaries catch rounding and validation mistakes that a single happy-path test misses.</p>
<p>Here Nest injects concrete classes because the service needs both implementations to choose per request. A TypeScript interface alone cannot serve as a runtime injection token; if a consumer needs one implementation behind an interface, <a href="https://docs.nestjs.com/fundamentals/custom-providers">Nest's custom providers</a> support an explicit token and <code>@Inject</code>. For a larger application, move HTTP error mapping to its boundary and keep pricing rules free of framework exceptions.</p>
<p>The <a href="https://github.com/amirtaherkhani/nestjs-skills/blob/main/skills/nestjs-oop-design-patterns/SKILL.md">NestJS Skills OOP and Design Patterns skill</a> includes guidance on when Strategy is warranted. Its <a href="https://github.com/amirtaherkhani/nestjs-skills/blob/main/skills/nestjs-oop-design-patterns/references/pattern-catalog.md#strategy">executable pattern catalog</a> includes a separate delivery-pricing Strategy example with Nest wiring and behavioral checks. The <a href="https://amirtaherkhani.github.io/nestjs-skills/">NestJS Skills documentation</a> explains how to find and use the skill.</p>
]]></content:encoded></item><item><title><![CDATA[Finding a Business-Rule Bypass in a NestJS Module]]></title><description><![CDATA[A wallet starts with a balance of 100. A checkout endpoint rejects a charge of 150. A bulk-charge endpoint accepts the same amount and leaves the balance at -50.
Both endpoints belong to the same Nest]]></description><link>https://nestjs-skills.hashnode.dev/finding-a-business-rule-bypass-in-a-nestjs-module</link><guid isPermaLink="true">https://nestjs-skills.hashnode.dev/finding-a-business-rule-bypass-in-a-nestjs-module</guid><category><![CDATA[nestjs]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[software architecture]]></category><dc:creator><![CDATA[amir taherkhani]]></dc:creator><pubDate>Wed, 07 Oct 2026 06:31:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ac3375e15779f8260ce464b/2bd471fa-e864-4347-b2af-a2081e9d6f46.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A wallet starts with a balance of 100. A checkout endpoint rejects a charge of 150. A bulk-charge endpoint accepts the same amount and leaves the balance at -50.</p>
<p>Both endpoints belong to the same NestJS application. The validation already exists. The second endpoint simply takes a path around it.</p>
<p>This is a useful kind of defect to look for during a module-boundary review: a consumer that can change another capability's state without going through the operation that protects its rules.</p>
<h2>Define the business rule</h2>
<p>For this small example, a charge has two requirements:</p>
<ul>
<li>The amount must be a finite, positive number.</li>
<li>The resulting balance must remain nonnegative.</li>
</ul>
<p>The storage provider holds the balance. The public operation applies the rules before writing:</p>
<pre><code class="language-typescript">@Injectable()
class WalletStorage {
  private balance = 100;

  read() {
    return this.balance;
  }

  write(balance: number) {
    this.balance = balance;
    return { balance };
  }
}

@Injectable()
class WalletOperations {
  constructor(
    @Inject(WalletStorage)
    private readonly storage: WalletStorage,
  ) {}

  charge(amount: number) {
    if (!Number.isFinite(amount) || amount &lt;= 0) {
      throw new BadRequestException('Positive amount required');
    }

    const next = this.storage.read() - amount;

    if (next &lt; 0) {
      throw new ConflictException('Insufficient balance');
    }

    return this.storage.write(next);
  }
}
</code></pre>
<p>These excerpts come from a <a href="https://github.com/amirtaherkhani/nestjs-skills/tree/96fb9a22c59a54fa1250a2664455c71df4942dfc/examples/module-boundary">runnable module-boundary example</a>. Imports and bootstrap code are in the linked files. It is an in-memory learning fixture, so the small balance is just test data.</p>
<p>Check whether every consumer uses <code>charge()</code> to change the balance.</p>
<h2>Trace the module boundary</h2>
<p>The original module exports both providers:</p>
<pre><code class="language-typescript">@Module({
  providers: [WalletStorage, WalletOperations],
  exports: [WalletStorage, WalletOperations],
})
class WalletModule {}
</code></pre>
<p>In NestJS, providers are encapsulated by their module unless they are made available through its exports. Those exports define what an importing module can inject. <a href="https://docs.nestjs.com/modules">NestJS module documentation</a></p>
<p>Both controllers are registered in <code>AppModule</code>, which imports <code>WalletModule</code>:</p>
<pre><code class="language-typescript">@Module({
  imports: [WalletModule],
  controllers: [CheckoutController, BulkController],
})
class AppModule {}
</code></pre>
<p>The controllers consume the wallet through another module's exports. A controller registered inside <code>WalletModule</code> could still inject its internal storage provider.</p>
<h3>Compare the safe path with the bypass</h3>
<p>The checkout controller injects <code>WalletOperations</code> and calls <code>charge()</code>. That path preserves the rule.</p>
<p>The bulk controller injects the lower-level storage provider:</p>
<pre><code class="language-typescript">@Controller('bulk')
class BulkController {
  constructor(
    @Inject(WalletStorage)
    private readonly storage: WalletStorage,
  ) {}

  @Post('charge')
  charge(@Body('amount') amount: number) {
    return this.storage.write(this.storage.read() - amount);
  }
}
</code></pre>
<p>With a starting balance of 100 and an amount of 150, this method writes -50. Nothing in this path invokes the checks in <code>WalletOperations</code>.</p>
<h3>Build a finding from evidence</h3>
<p>The defect follows this path:</p>
<ol>
<li><code>WalletModule</code> exposes a provider that can write the balance directly.</li>
<li><code>BulkController</code> injects that provider.</li>
<li>Its handler performs a subtraction and writes the result.</li>
<li>The charge rules live in a different operation that this handler never calls.</li>
</ol>
<p>An exported provider by itself is only a place to investigate. Here, the consumer and the broken rule establish the defect. The <a href="https://github.com/amirtaherkhani/nestjs-skills/blob/96fb9a22c59a54fa1250a2664455c71df4942dfc/examples/module-boundary/before/app.ts#L5-L35">original application</a> contains the complete path.</p>
<h2>Fix the consumer and the public module API</h2>
<p>Two small changes close this path.</p>
<h3>Route bulk charges through the public operation</h3>
<p>Make the bulk controller use the same public operation as checkout:</p>
<pre><code class="language-typescript">@Controller('bulk')
class BulkController {
  constructor(
    @Inject(WalletOperations)
    private readonly wallet: WalletOperations,
  ) {}

  @Post('charge')
  charge(@Body('amount') amount: number) {
    return this.wallet.charge(amount);
  }
}
</code></pre>
<h3>Remove the storage export</h3>
<p>Remove the storage provider from the module's public exports:</p>
<pre><code class="language-typescript">@Module({
  providers: [WalletStorage, WalletOperations],
  exports: [WalletOperations],
})
class WalletModule {}
</code></pre>
<p><code>WalletStorage</code> remains available inside <code>WalletModule</code>. Consumers that import the module get the operation that enforces its rules. You can compare the <a href="https://github.com/amirtaherkhani/nestjs-skills/blob/96fb9a22c59a54fa1250a2664455c71df4942dfc/examples/module-boundary/after/app.ts#L5-L35">corrected application</a> with the original.</p>
<p>Changing only the controller repairs this consumer but leaves the shortcut available. Removing only the export breaks dependency resolution for the existing consumer. Review and change both sides together.</p>
<h3>Why reuse the existing operation?</h3>
<p>Copying the checks into the bulk controller would fix this request, but it would create a second place to maintain the same rule.</p>
<p>Moving domain rules into an entity can be useful in a richer model. This fixture already has a small public operation that owns the charge, so reusing it closes the demonstrated path without adding another layer.</p>
<p>Before removing an export in a real project, find all of its consumers. Each caller may need migration, and an exported provider in a published library may already be part of its supported API.</p>
<h2>Test through both HTTP paths</h2>
<p>A unit test of <code>WalletOperations.charge()</code> can pass while the bulk endpoint remains broken. It proves that the operation rejects an overdraft when called. It does not prove that every endpoint calls it.</p>
<p>The example therefore sends the same invalid charge through both routes:</p>
<pre><code class="language-javascript">test('healthy public-operation consumer rejects an overdraft', async () =&gt; {
  await request(app.getHttpServer())
    .post('/checkout/charge')
    .send({ amount: 150 })
    .expect(409);
});

test('bulk consumer must preserve the same nonnegative-balance invariant', async () =&gt; {
  await request(app.getHttpServer())
    .post('/bulk/charge')
    .send({ amount: 150 })
    .expect(409);
});
</code></pre>
<p>The checkout test is a healthy control. It shows that the public operation already handles the case. The bulk test distinguishes the unsafe path from the safe one.</p>
<p>A third test submits a valid checkout charge of 20 and checks that the returned balance is 80. Each test starts a fresh application and closes it afterward, keeping a previous write from affecting the next test. The <a href="https://github.com/amirtaherkhani/nestjs-skills/blob/96fb9a22c59a54fa1250a2664455c71df4942dfc/examples/module-boundary/contract.test.mjs">complete test file</a> includes that setup.</p>
<p>For a stronger application test suite, also verify a successful bulk charge, check that a rejected charge leaves state unchanged, and cover invalid amounts through every public entry point. Those would extend the three checks in this fixture.</p>
<h3>Run the example</h3>
<p>To run the repository's examples in a fresh checkout with Node.js 20 or later:</p>
<pre><code class="language-bash">git clone https://github.com/amirtaherkhani/nestjs-skills.git
cd nestjs-skills
git checkout 96fb9a22c59a54fa1250a2664455c71df4942dfc
npm ci
npm run test:fixtures
</code></pre>
<p>The pinned revision keeps the example aligned with this article. The command above runs all three learning fixtures. The <a href="https://github.com/amirtaherkhani/nestjs-skills/blob/96fb9a22c59a54fa1250a2664455c71df4942dfc/scripts/test-fixtures.mjs">runner</a> expects the original versions to fail their seeded regression checks and the corrected versions to pass. An unexpected result fails the runner.</p>
<h3>What the tests verified</h3>
<p>For this article, the TypeScript examples were compiled and the existing module-boundary test file was run against each wallet version on Node.js 24.11.0, NestJS 11.2.7, and TypeScript 5.9.3:</p>
<table>
<thead>
<tr>
<th>Check</th>
<th>Original</th>
<th>Corrected</th>
</tr>
</thead>
<tbody><tr>
<td>Checkout rejects a charge of 150</td>
<td>Pass: 409</td>
<td>Pass: 409</td>
</tr>
<tr>
<td>Bulk rejects a charge of 150</td>
<td>Expected failure: received 201</td>
<td>Pass: 409</td>
</tr>
<tr>
<td>Checkout charges 20 and returns a balance of 80</td>
<td>Pass</td>
<td>Pass</td>
</tr>
</tbody></table>
<p>The original version has two passes and one intentional failure; the corrected version has three passes. Only the wallet HTTP tests were executed for this article review. These results verify the fixture's behavior, not the effectiveness of an AI review tool.</p>
<h2>Scope and limits</h2>
<p>The repair gives this example a narrower module API and routes both charge endpoints through the same checks.</p>
<p>It does not establish that every storage provider in every NestJS application should be hidden. Some exported providers are intentionally shared infrastructure or a carefully designed read interface. Review the operations they expose, their consumers, and the rules those consumers must preserve.</p>
<p>The example has one in-memory balance and synchronous writes. A real persisted wallet needs its own decisions about atomic updates, concurrent requests, authorization, numeric representation, and retry behavior.</p>
<p>Removing a module export does not solve those concerns. Nest module encapsulation controls dependency injection; it is not a security sandbox against code running in the same process.</p>
<h2>Apply the review method to your own codebase</h2>
<p>Choose one business rule and trace every path that can change the state it protects:</p>
<ul>
<li>Where is the rule enforced?</li>
<li>Which providers can write the underlying state?</li>
<li>Which of those providers are exported?</li>
<li>Do any consumers reach the write without passing through the rule?</li>
<li>Which boundary test would demonstrate that behavior?</li>
</ul>
<p>Then describe the finding with the call path, observable impact, smallest correction, and regression test. That gives a reviewer enough information to check your conclusion.</p>
<h2>Key takeaways</h2>
<ul>
<li>Trace a write from the consumer to the business rule before reporting a module-boundary defect.</li>
<li>Repair both the bypassing consumer and the module API that exposes the shortcut.</li>
<li>Test every relevant entry point: a passing operation test cannot prove that a controller calls it.</li>
</ul>
<h2>A tool for repeating this review</h2>
<p>The <a href="https://github.com/amirtaherkhani/nestjs-skills/blob/main/skills/nestjs-code-audit/SKILL.md"><code>nestjs-code-audit</code> skill</a> provides a read-only workflow for collecting and reporting this evidence. You can ask it to trace exported providers and their consumers, identify invariant bypasses, and propose a regression test without changing code. Each finding still needs to hold up against the application.</p>
<p>The <a href="https://amirtaherkhani.github.io/nestjs-skills/">NestJS Skills documentation</a> explains how to install the skills and choose the right one for a specific engineering task.</p>
]]></content:encoded></item></channel></rss>