Головна Наші публікації
Модульний моноліт на NestJS: практичний посібник

Модульний моноліт на NestJS: практичний посібник

  • NestJS
  • modular monolith
  • bounded modules
  • domain events
  • transactions
  • architecture testing
  • microservices
Модульний моноліт на NestJS: практичний посібник

Проєктування bounded modules, public APIs, transactions, events, dependency rules і тестів із безпечним шляхом до виділення сервісів.

Модульний моноліт на NestJS: практичний посібник

Модульний моноліт — це один deployable application, бізнес-можливості якого розділені enforceable boundaries. Він зберігає local calls, просте розгортання та database transactions, водночас запобігаючи неконтрольованому coupling, що перетворює звичайний моноліт на проблему. Для багатьох продуктів, що розвиваються, це кращий default, ніж старт із distributed system.

NestJS надає modules, providers і dependency injection, але decorators самі не створюють domain boundaries. Команда має визначити ownership, public contracts, data access і дозволені dependencies. GARNO.TECH застосовує ці правила в межах розробки на NestJS, зберігаючи швидкість delivery та створюючи реальний шлях до майбутнього виділення сервісів.

Починайте з бізнес-меж, а не з папок

Будуйте modules навколо бізнес-можливостей Orders, Billing, Inventory та Identity, а не навколо технічних layers, спільних для всього application. Bounded module володіє своїми use cases, domain rules, persistence mappings і migrations. Controllers є delivery adapters, а repositories — implementation details. Generic shared folder не повинен ставати прихованим domain без ownership.

Визначте мову та invariants кожної boundary. Orders може зберігати customer і product identifiers, але не має звертатися до repository іншого module, щоб змінити його tables. Іноді малий stable value object краще продублювати, ніж зв’язати release decisions. Справді generic infrastructure — logging, configuration, clock і tracing — тримайте у вузькому core без business policy.

Надайте кожному модулю малий public API

Інші modules мають імпортувати явний facade, command/query port або event contract, але не internal services і repositories. NestJS providers інкапсульовані, якщо їх не export, тому вважайте exports array архітектурною public surface. Експортуйте лише кілька stable capabilities, а controllers, ORM entities, repository implementations та internal orchestration залишайте private.

Facade приймає purpose-specific input і повертає contract, яким володіє boundary. Не виставляйте query builders або persistence entities: тоді callers залежать від storage details. Якщо для business decision потрібен synchronous call, використовуйте public port. Якщо consumer лише реагує на факт, публікуйте event, назва якого описує завершену business occurrence.

// orders/public/orders.facade.ts
export abstract class OrdersFacade {
  abstract place(command: PlaceOrder): Promise<PlacedOrder>;
  abstract find(id: string): Promise<OrderView | null>;
}

// orders/orders.module.ts
@Module({
  imports: [OrdersPersistenceModule],
  controllers: [OrdersController],
  providers: [
    PlaceOrderHandler,
    FindOrderHandler,
    { provide: OrdersFacade, useClass: NestOrdersFacade }
  ],
  exports: [OrdersFacade] // the complete public module API
})
export class OrdersModule {}

// Billing imports OrdersFacade; it never imports OrdersRepository,
// OrderEntity or handlers from an orders/internal path.

Залишайте transactions локальними та надійно публікуйте events

Зберігайте invariant і його writes в одному module та одній local transaction. Transaction, що охоплює tables кількох owners, приховує проблему boundary і стане складною після extraction. Коли operation має замовити роботу в іншому місці, спершу commit-те owner state, а потім координуйте workflow з явними failure та compensation rules.

In-process events допомагають decoupling, але publish лише після commit створює crash window, а до commit може описати data, що потім rollback. Для важливих integration events запишіть outbox row в одній transaction із domain state. Worker публікує її з idempotency key, записує delivery attempts і витримує duplicates. Consumers мають бути idempotent, навіть поки все працює в одному process.

await dataSource.transaction(async manager => {
  const order = Order.place(command);
  await manager.getRepository(OrderEntity).save(mapOrder(order));

  await manager.getRepository(OutboxMessage).insert({
    id: randomUUID(),                  // consumer idempotency key
    type: "orders.order-placed.v1",
    aggregateId: order.id,
    payload: JSON.stringify({
      orderId: order.id,
      customerId: order.customerId,
      totalMinor: order.totalMinor
    }),
    occurredAt: new Date(),
    publishedAt: null
  });
});

// A separate worker claims unpublished rows, publishes them,
// and marks them delivered. Retries may create duplicates,
// so every consumer records processed message IDs.

Перетворіть dependency rules на виконувані тести

Architecture documentation застаріває, якщо CI її не enforce. Додайте rules, які забороняють imports з internal directory іншого module, не дозволяють domain code залежати від NestJS або ORM packages і відхиляють cycles. Дозволяйте cross-module imports лише через public entry point. Поєднуйте static rules з module-level integration tests і real persistence там, де важлива transaction behavior.

Тестуйте boundary через public API, а не private providers. Contract tests мають охоплювати commands, queries і event schemas. Малий end-to-end suite перевіряє wiring та HTTP behavior, але не замінює швидких domain tests. Вважайте forbidden-import violation помилкою build, бо warning, який можна проігнорувати, не є architectural constraint.

Виділяйте сервіс лише за наявності доказів

Готуйтеся до extraction, не вдаючи, що network уже існує. Надайте candidate module чіткий ownership, serializable contracts, module-owned data access, observable workflows та idempotent event handling. Не додавайте HTTP між modules, distributed tracing та eventual consistency лише для схожості з microservices: ці витрати мають з’явитися після реальної потреби.

Виділяйте service, коли stable capability потребує independent deployment, суттєво іншого scaling, сильнішої security/failure isolation або autonomous team ownership. Спочатку виміряйте constraint і визначте SLO. Потім сховайте public contract за transport adapter, перенесіть owned data, запустіть compatibility tests, поступово перемикайте traffic і збережіть rollback. Безпечною міграцію роблять boundaries, а не speculative infrastructure.

Чекліст реалізації модульного моноліту

  • Визначте business capabilities, invariants, language і data ownership до створення NestJS modules.
  • Відкрийте малий facade, port чи event contract; repositories, entities та handlers залиште private.
  • Тримайте transactions усередині одного owner; для надійних integration events використовуйте transactional outbox.
  • Enforce дозволені imports, layer direction і відсутність cycles у CI; тестуйте modules через public APIs.
  • Extract лише за виміряної потреби в deployment, scaling, isolation або ownership, із telemetry та rollback.
NestJS-моноліту потрібні enforceable boundaries?
Покажіть module graph, data ownership і deployment constraints. Ми знайдемо coupling і визначимо incremental modularization path.
Публікації

Наші дослідження

Дослідження та розробка рішень на основі штучного інтелекту для оптимізації бізнес-процесів і підвищення ефективності прийняття рішень.

Аналіз моделей машинного навчання для прогнозної аналітики у фінансовій сфері, електронній комерції та SaaS-платформах.

Дослідження технологій обробки природної мови та комп'ютерного зору для посилення автоматизації, персоналізації та підтримки клієнтів.

Ми використовуємо файли cookie для забезпечення безпеки та належної роботи нашого сайту. За вашою згодою ми також використовуємо необов'язкові файли cookie для аналітики та рекламних цілей. Ви можете прийняти або відхилити використання необов'язкових файлів cookie. Ви можете змінити свої налаштування в будь-який час. Докладніше — у нашій Політиці щодо файлів cookie.