Skip to content

Instantly share code, notes, and snippets.

@joshuamabina
Created December 31, 2025 06:35
Show Gist options
  • Select an option

  • Save joshuamabina/fec4745ebcbb25ad6eadf156230331e2 to your computer and use it in GitHub Desktop.

Select an option

Save joshuamabina/fec4745ebcbb25ad6eadf156230331e2 to your computer and use it in GitHub Desktop.
Building Testable Node.js Apps

Building Testable Node.js Apps

This Gist serves as an initial implementation of a crash course designed for building testable Node.js Apps.

Included in this document:

  • The Hexagonal Premise: Inversion of Control and the sanctity of the Domain.
  • The Domain Model: Distinguishing Entities from Values.
  • Aggregates: Defining consistency boundaries and the Aggregate Root.
  • Domain Services: Handling logic that belongs to no single object.
  • Ports and Adapters: Defining interfaces (Ports) and technical implementations (Adapters).
  • Repository Pattern: Decoupling the Domain from the Persistence layer.
  • Test-Driven Development (TDD): Testing the "pure" domain without side effects.

Module 0: Introduction to the Hexagonal Mindset

In modern enterprise Node.js development, the greatest threat to project longevity is entanglement. When your business logic is married to an ORM or a specific web framework, the cost of change scales exponentially.

Hexagonal Architecture (or Ports and Adapters) addresses this by placing the "Domain" at the center.

  • The Core: Contains the business rules (Entities, Values, Services). It has no dependencies on external libraries.
  • The Ports: Interfaces that define how the outside world interacts with the core (Inbound) and how the core interacts with the world (Outbound).
  • The Adapters: Concrete implementations (Express.js, PostgreSQL, Stripe SDK) that plug into these ports.

Module 1: The Core Domain – Orders

In this module, we define the Order aggregate. We utilize Values for attributes that are defined by their properties rather than a unique identity.

Structural Decisions

  • The src/domain/orders/values folder: Stores immutable objects like Money and Address.
  • The src/domain/orders/repositories folder: Contains the Port (interface) for the Order Repository.
  • The data folder: This is an infrastructure concern (e.g., /src/infrastructure/database/data) used for seed files or local JSON persistence.

Implementation: The Money Value

The Money class represents a Value. It is immutable; any operation (like addition) returns a new instance. It ensures that we never perform arithmetic on mismatched currencies.


'use strict';

class Money {
  constructor(amount, currency) {
    if (typeof amount !== 'number') throw new Error('Amount must be a number');
    this.amount = amount;
    this.currency = currency;
    Object.freeze(this); // Ensure immutability
  }

  equals(other) {
    return this.amount === other.amount && this.currency === other.currency;
  }

  add(other) {
    if (this.currency !== other.currency) {
      throw new Error('Currency mismatch');
    }
    return new Money(this.amount + other.amount, this.currency);
  }
}

module.exports = Money;

The Order Aggregate

'use strict';

const Money = require('./values/Money');

class Order {
  #id;
  #status;
  #items;

  constructor(id, customerId) {
    this.#id = id;
    this.customerId = customerId;
    this.#status = 'CREATED';
    this.#items = [];
  }

  addItem(productId, price) {
    if (this.#status !== 'CREATED') throw new Error('Cannot modify confirmed order');
    this.#items.push({ productId, price });
  }

  confirm() {
    if (this.#items.length === 0) throw new Error('Cannot confirm empty order');
    this.#status = 'CONFIRMED';
  }

  cancel() {
    this.#status = 'CANCELLED';
  }

  get total() {
    return this.#items.reduce(
      (sum, item) => sum.add(item.price), 
      new Money(0, 'USD')
    );
  }

  toJSON() {
    return { id: this.#id, status: this.#status, total: this.total };
  }
}

module.exports = Order;

Module 2: The Delivery Domain

The Delivery domain focuses on the transition from an Order to a physical movement. Here we introduce the AssignmentService, a Domain Service that coordinates between a Delivery and a Driver.

Implementation: Delivery Aggregate

'use strict';

class Delivery {
  constructor(id, orderId, location) {
    this.id = id;
    this.orderId = orderId;
    this.location = location;
    this.driverId = null;
    this.status = 'PENDING';
  }

  assignDriver(driverId) {
    this.driverId = driverId;
    this.status = 'ASSIGNED';
  }
}

module.exports = Delivery;

TDD Exercise: Assignment Logic

TODO

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment