If you are building enterprise-grade Node.js applications with TypeScript, chances are you are already deeply familiar with NestJS. It has become the de-facto standard for opinionated, heavily structured, Angular-inspired backend development.
But the ecosystem is always evolving, and a formidable contender has entered the arena: Ditsmod.
Like NestJS, Ditsmod relies on decorators, Dependency Injection (DI), and a modular architecture. However, under the hood, it takes a noticeably different, highly explicit approach to provider scoping, extension initialization, and request lifecycles.
In this first article of the "NestJS vs. Ditsmod" series, we will explore the foundational architectural differences between the two frameworks.
1. Modules and explicit architecture
Both frameworks organize code into logical, reusable blocks called Modules.
In NestJS, you use the @Module() decorator to declare imports, controllers, providers, and exports.
Ditsmod splits this responsibility into specialized roles, making the application graph highly explicit:
-
@rootModule(usuallyAppModule) is the single entry point. -
@featureModuleencapsulates reusable providers. - Specialized module decorators like
@restModuleor@trpcModuleadd transport-specific capabilities (likecontrollersand routeappends).
Collision Resolution
One of the most distinct architectural choices in Ditsmod is how it handles token collisions. If you import two modules in NestJS that export the same provider token, the framework resolves it silently based on import order.
Ditsmod explicitly rejects this ambiguity. If a collision occurs, Ditsmod throws an error and forces you to resolve it manually using arrays like resolvedCollisionsPer*. This strictness prevents hard-to-debug DI overrides in massive codebases.
2. Dependency Injection (DI) Hierarchies
NestJS handles provider scope using the Scope enum (DEFAULT, REQUEST, TRANSIENT). A Request-scoped provider in NestJS bubbles up, causing any service that injects it to also become Request-scoped, which can sometimes lead to performance bottlenecks.
Ditsmod approaches DI through a strict Injector Hierarchy. There are four distinct levels of injectors:
-
providersPerApp(Application singleton) -
providersPerMod(Module singleton) -
providersPerRou(Route singleton) -
providersPerReq(HTTP request instance)
The rule is simple but powerful: Child injectors look up to parents, but parents never look down. If a service is registered in providersPerApp, it cannot inject a service from providersPerReq. Furthermore, if you override a provider at the Req level, it shadows the parent provider without affecting the App or Mod levels.
3. Extending the Framework: Lifecycle vs. Extensions
To hook into the framework setup, NestJS provides lifecycle interfaces: OnModuleInit, OnApplicationBootstrap, etc.
Ditsmod completely redesigns this concept with its Extensions System. In Ditsmod, extensions are isolated infrastructure hooks that run after metadata collection but before the request handlers are created.
Instead of simple lifecycle hooks, Ditsmod extensions implement up to three sequential stages:
-
stage1(isLastModule): Used to dynamically collect metadata, build routes, and push providers into arrays dynamically. -
stage2(injectorPerMod): Runs afterstage1completes for all modules, receiving a fully formed module-level injector. -
stage3(): Final post-processing.
Furthermore, Ditsmod extensions can form Groups. Multiple extensions can contribute data to a single "Lead Extension," allowing plugins (like OpenAPI generators or custom body parsers) to seamlessly weave into the routing pipeline without messy workarounds.
4. The REST Request Lifecycle
In NestJS, an incoming HTTP request flows through a well-known pipeline:
Middleware -> Guards -> Interceptors -> Pipes -> Controllers -> Filters.
Ditsmod manages HTTP requests via the @ditsmod/rest package, executing a streamlined, nested interceptor chain managed by the RequestDispatcher. The flow is:
-
HttpFrontend: Parses path and query parameters. -
GuardedInterceptor: ExecutesCanActivateguards. (If a guard fails, execution stops immediately). - Custom `HTTP_INTERCEPTORS`: User-defined or module-defined interceptors.
-
HttpBackend: Invokes the actual controller method.
If you need to wrap the entire request (for instance, to set up an OpenTelemetry trace that includes the Guard execution), Ditsmod allows you to override the RequestDispatcher entirely at the providersPerApp level.
5. Task Scheduling
Both frameworks offer elegant, decorator-driven task scheduling using cron jobs, timeouts, and intervals.
In NestJS, you import the ScheduleModule and use @Cron(), @Interval(), or @Timeout().
Ditsmod provides @ditsmod/schedule with practically identical decorators (@cron, @interval, @timeout) and a SchedulerRegistry for runtime management. However, true to its architectural strictness, Ditsmod enforces DI scope constraints on your scheduled tasks:
Task classes MUST be registered in providersPerMod or providersPerApp.
If you attempt to register a cron job in providersPerRou or providersPerReq, Ditsmod will actively block it and emit a warning, preventing memory leaks and orphaned intervals that often plague request-scoped scheduling.
What's Next?
NestJS is a fantastic, battle-tested framework. However, Ditsmod's highly explicit DI hierarchy, strict collision handling, and powerful three-stage Extension system present a compelling alternative for developers who want absolute control over their application's infrastructure and dependency graph.
In Part 2 of this series, we will dive deeper into the code. We will compare how to write custom interceptors, handle complex parameter validation (Pipes vs. Ditsmod Factory Providers), and benchmark the request lifecycle.
Have you tried Ditsmod yet? Let me know your thoughts on its strict DI approach in the comments!
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.