DEV Community

Cover image for NestJS v12.0 vs. Ditsmod v3.0: Dynamic Modules, Providers, and the Power of Extension Groups (Part 3)
Костя Третяк
Костя Третяк

Posted on

NestJS v12.0 vs. Ditsmod v3.0: Dynamic Modules, Providers, and the Power of Extension Groups (Part 3)

Welcome to Part 3 of the "NestJS v12.0 vs. Ditsmod v3.0" series! In our previous articles, we explored the strict Dependency Injection hierarchies and modern validation pipelines of these two cutting-edge Node.js frameworks.

Today, we are tackling the true hallmark of an enterprise framework: Extensibility.

When building large-scale applications, you inevitably need to write reusable libraries, configurable plugins, and complex infrastructure hooks (like OpenAPI generators, custom routers, or database orchestrators). Both NestJS and Ditsmod draw heavy inspiration from Angular's architectural playbook to solve this, but their execution diverges dramatically as application complexity scales.

Let's explore how Dynamic Modules, Dynamic Providers, and Ditsmod’s unique Extension Groups compare.


The Shared Ancestry: Dynamic Modules & Dynamic Providers

If you have ever used Angular, you are likely familiar with the RouterModule.forRoot() pattern. This is a Dynamic Module—a module that doesn't just statically register providers, but accepts configuration at runtime to dynamically generate providers before the DI container is built.

NestJS Dynamic Modules

NestJS fully embraces this concept. You return a DynamicModule object containing providers, exports, and module from a static factory method.

import { Module, DynamicModule } from '@nestjs/common';

@Module({})
export class DatabaseModule {
  static forRoot(connectionString: string): DynamicModule {
    return {
      module: DatabaseModule,
      providers: [
        {
          provide: 'CONNECTION_STRING',
          useValue: connectionString,
        },
      ],
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Ditsmod Dynamic Modules

Ditsmod provides the exact same architectural capability, rooted in the same Angular philosophy. Ditsmod supports importing modules in two forms: Static Modules (a raw class reference) and Dynamic Modules (an object implementing the DynamicModule interface).

Using Dynamic Modules in Ditsmod, configuration details, route prefixes, or custom provider overrides can be supplied directly at the import site.

import { type DynamicModule } from '@ditsmod/core';
import { SomeService } from './some.service.js';

export class UsersModule {
  // A static helper method returning a DynamicModule
  static withConfig(path: string): DynamicModule<UsersModule> {
    return { 
      module: this, 
      path, 
      providersPerReq: [{ token: 'CUSTOM_CONFIG', useValue: { path } }] 
    };
  }
}

Enter fullscreen mode Exit fullscreen mode

In both frameworks, Dynamic Modules are excellent for initializing dynamic providers (like injecting a specific API key or database configuration).

However, what happens when you need to do something more complex? What if you need to scan the entire application for specific decorators, dynamically generate routes, or aggregate metadata from ten different third-party modules to build a Swagger file?

NestJS: Lifecycle Hooks & The Discovery Service

In NestJS, complex framework extensions are typically handled using a combination of Lifecycle Hooks (OnModuleInit, OnApplicationBootstrap) and the DiscoveryService.

If you are building an OpenAPI generator in NestJS, your logic lives inside an OnApplicationBootstrap hook. You inject the DiscoveryService, iterate over every registered controller and provider, read their Reflect metadata using the Reflector, and dynamically build your schema.

While this approach works, it has limitations:

  1. It occurs at runtime (after the DI container is fully instantiated).
  2. You cannot easily inject new providers into the application based on what you discover.
  3. Coordination between multiple independent plugins requires complex workarounds.

Ditsmod v3.0: The Extension System & Groups

Ditsmod abandons runtime lifecycle discovery for infrastructure setup. Instead, it introduces a highly structured Extension System.

Extensions operate strictly during the application initialization stage to set up infrastructure and do not participate directly in request processing. They run after static metadata is collected from decorators, but before request handlers and DI containers are fully finalized.

The Three-Stage Pipeline

Every Ditsmod Extension can implement up to three sequential framework lifecycle hooks that are always executed in order:

  1. stage1(isLastModule): Used to dynamically collect metadata, push new providers into the providersPerApp, providersPerMod, providersPerRou, or providersPerReq arrays, and aggregate data.

  2. stage2(injectorPerMod): Called after stage1 finishes for ALL modules, receiving a fully prepared module-level injector.

  3. stage3(): Used for final post-processing before the application starts.

Because stage1 executes while provider arrays are still being assembled, a Ditsmod extension can read a module's configuration and dynamically push an interceptor directly into that specific route's providersPerReq array. You are modifying the application graph before it is sealed.

Extension Groups: The True Game Changer

The most powerful feature of Ditsmod's extensibility is Extension Groups.

An extension group allows multiple independent extensions to contribute data under the class token of a "Lead Extension". For example, if you have a core RestRouteExtension that handles routing, other third-party extensions can hook into it simply by declaring it in their groups array:

// 1. A third-party OpenAPI extension joins the RestRouteExtension group
export const OpenApiExtensionConfig = {
  extension: OpenApiRouteExtension,
  groups: [RestRouteExtension], // Joins the group seamlessly
  export: true,
};

Enter fullscreen mode Exit fullscreen mode

When an extension joins a group, it automatically inherits the group's positioning relative to other extensions in the execution graph.

Later, when the some extension asks the ExtensionManager for its stage1 data, the manager returns an aggregated payload containing data from the Lead Extension and all member extensions registered in its group.

This means extensions can share a base interface contract and collaboratively build infrastructure (like appending Swagger documentation metadata to core route metadata) without being tightly coupled or requiring a global DiscoveryService hack.


Conclusion

Both NestJS and Ditsmod provide excellent tools for basic configurability via Angular-inspired Dynamic Modules and Dynamic Providers.

However, when building complex architectural plugins:

  • NestJS relies on runtime scanning via DiscoveryService and lifecycle hooks, which can be limiting if you need to mutate the DI container dynamically.
  • Ditsmod solves this with a dedicated compile-time-like Extension System. By utilizing stage1 hooks and Extension Groups, Ditsmod allows plugins to collaboratively aggregate metadata and dynamically push providers into the DI hierarchy before the application fully starts.

In Part 4 of the series, we will take a deep dive into Reflection APIs and Custom Decorators, exploring how both frameworks handle metadata merging and inheritance. Stay tuned!

Top comments (0)