Главная Наши публикации
Модульный монолит на 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.