Welcome to Part 4 of the "NestJS v12.0 vs. Ditsmod v3.0" series! In Part 3, we explored how both frameworks handle extensibility through Dynamic Modules and Extension Groups.
Today, we are looking at the invisible glue that holds both of these Node.js frameworks together: TypeScript Decorators and Metadata Reflection.
If you use NestJS or Ditsmod, your code is covered in decorators—@Controller(), @Injectable(), @Get(), etc. But what happens when you need to write your own custom decorators? How do these frameworks read, store, and merge that metadata under the hood?
Let's compare the traditional NestJS @SetMetadata approach with Ditsmod's highly engineered Reflector abstraction.
1. NestJS: The @SetMetadata Approach
NestJS provides a straightforward, accessible API for dealing with custom metadata, built heavily around the standard reflect-metadata package.
When you want to attach custom data to a route (for example, marking a route as "public" or assigning specific roles), you use @SetMetadata() or create a custom decorator factory:
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
@Controller('admin')
export class AdminController {
@Get('dashboard')
@Roles('super-admin', 'manager')
getDashboard() {
return { status: 'secure' };
}
}
To read this metadata later (usually inside a Guard or Interceptor), you inject the NestJS Reflector class:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) return true;
const request = context.switchToHttp().getRequest();
return roles.includes(request.user.role);
}
}
The Limitations:
This approach is simple and effective. However, it relies on basic key-value retrieval. If you have a complex OOP structure where a child controller extends a base CRUD controller, reflect-metadata (and by extension, the default NestJS reflector methods) does not automatically traverse the inheritance tree to merge metadata from the parent class down to the child. Developers often have to write manual fallback logic to check the prototype chain.
2. Ditsmod v3.0: The Unified Reflector Abstraction
Ditsmod takes a fundamentally different, heavily optimized approach to metadata. Instead of just wrapping reflect-metadata, Ditsmod's Reflector provides a unified, cached, and high-level abstraction over standard reflection metadata. (It uses the lighter reflect-metadata/lite polyfill under the hood, which is loaded automatically by @ditsmod/core).
Creating Type-Safe Decorators
Instead of raw key-value pairs, Ditsmod provides static factory methods to generate structured decorators: Reflector.makeClassDecorator(), Reflector.makePropDecorator(), and Reflector.makeParamDecorator().
You can pass a transformer function directly into the factory to validate and format the arguments:
import { Reflector } from '@ditsmod/core';
interface RoleOptions {
roles: string[];
strict?: boolean;
}
// Generates a property-level decorator and transforms its arguments
export const requireRoles = Reflector.makePropDecorator(
(roles: string[], strict = false) => ({ roles, strict } satisfies RoleOptions)
);
class AdminController {
@requireRoles(['super-admin', 'manager'], true)
secureData() {}
}
Inheritance Traversal (The Killer Feature)
One of the most powerful features of Ditsmod's Reflector is its native understanding of OOP inheritance. It automatically merges class, property, and parameter metadata from parent classes down to children.
When Ditsmod collects metadata for a class, it builds an internal mapping of the inheritance tree, tracking exactly where each decorator originated via the decoratorChain and paramChain fields of the MergedClassPropMeta object. If you extend a base controller, the child seamlessly inherits the parent's route and parameter decorators without any manual prototype hacking.
Constructor Inheritance: The ParentParams Token
One of the most frustrating aspects of standard TypeScript DI (including NestJS) is constructor inheritance. If your ChildController extends a BaseCrudController that has five injected services, your ChildController must explicitly declare all five of those services in its constructor just to pass them up via super(dep1, dep2, ...). If the base class changes, every child class breaks.
Because Ditsmod's Reflector natively understands inheritance chains, its Dependency Injection system can solve this automatically using the special ParentParams token.
When an @injectable() class extends a parent class, Ditsmod automatically gathers the parent's required dependencies and injects them as an array into ParentParams.
import { ParentParams, injectable, inject } from '@ditsmod/core/di';
@injectable()
class BaseController {
constructor(private logger: Logger, private db: DbService) {}
}
@injectable()
class ChildController extends BaseController {
constructor(
// We inject the parent's parameters cleanly without redeclaring them!
@inject(ParentParams) parentParams: ConstructorParameters<typeof BaseController>,
public childSpecificService: ChildSpecificService,
) {
// Spread the array into super()
super(...parentParams);
}
}
You can use the @inject(ParentParams) decorator for strict type safety, or use the @ts-expect-error shorthand approach. Either way, Ditsmod completely eliminates the brittle boilerplate of passing dependencies up the prototype chain!
Decorator Grouping (decoratorId)
When building complex plugins, you often have multiple decorators that belong to the same logical family. Ditsmod allows you to group them effortlessly.
When creating a decorator, you can pass a reference to a "base" decorator as the third argument (the decoratorId).
import { Reflector, DecoratorMeta } from '@ditsmod/core';
// 1. Create a base decorator (acts as the Group ID)
export const apiGroup = Reflector.makeClassDecorator((data?: any) => data, 'apiGroup');
// 2. Create related decorators, passing `apiGroup` as the decoratorId
export const apiModel = Reflector.makeClassDecorator((data?: any) => data, 'apiModel', apiGroup);
export const apiEntity = Reflector.makeClassDecorator((data?: any) => data, 'apiEntity', apiGroup);
Later, you can extract all metadata associated with that specific group identifier, making it incredibly easy to parse large, multi-decorator structures for things like OpenAPI documentation generation.
Iterability and Caching
When you need to read an entire class at once, Ditsmod provides Reflector.collectMeta(Cls). This returns a MergedClassMeta object which is fully iterable. You can loop through it to get property-by-property details, constructor parameters, and inheritance maps, all of which are resolved once and cached internally for maximum performance.
Conclusion
Both frameworks provide excellent tools for metadata reflection, but they are built for slightly different developer experiences:
-
NestJS v12.0 maintains its highly approachable
@SetMetadataandReflector.get()pattern. It is incredibly easy to learn and covers 95% of standard web application use cases perfectly. -
Ditsmod v3.0 treats metadata reflection as a core infrastructure pillar. By providing automated inheritance traversal, decorator grouping via
decoratorId, and a strongly typed, iterableMergedClassMetaobject, Ditsmod completely eliminates the boilerplate required to parse complex OOP structures.
If you are building simple guards, NestJS's approach is quick and painless. But if you are building complex libraries, automated code generators, or heavily abstracted base classes, Ditsmod’s Reflection API feels like a superpower.
In Part 5 of the series, we will wrap up by exploring Application Lifecycle and Graceful Shutdown — comparing how both frameworks safely drain HTTP connections and close database pools in production environments. Stay tuned!
Top comments (0)