@toss/nestjs-aop
v2.2.1
Published
Readme
English | 한국어
Features
- 🎯 Decorator-based AOP — wrap any method, getter, or setter with reusable cross-cutting logic (caching, logging, retries, ...)
- 🧩 Plays nice with the IoC container — your aspect is a regular NestJS provider, so
@Injectanything you need - 🪶 Zero runtime magic — built on native decorators +
reflect-metadata, no code transformation step - ✅ NestJS 8 → 12 — one package, no major-version treadmill
- 🧬 Inheritance-aware — decorated methods keep working when called through a subclass
Installation
npm install @toss/nestjs-aop
pnpm add @toss/nestjs-aop
yarn add @toss/nestjs-aopUsing NestJS 12? NestJS 12 ships as ESM only, while this package is published as CommonJS. Loading it therefore relies on Node's
require(esm)support, so NestJS 12 users need Node.js^20.19.0 || >=22.12.0. On Node 20.x below 20.19.0 or Node 22.x below 22.12.0, the import fails withERR_REQUIRE_ESM. NestJS 8 - 11 users are unaffected.
Quick Start
export const CACHE = Symbol('CACHE');
export const Cache = (options?: CacheOptions) => createDecorator(CACHE, options);
@Aspect(CACHE)
export class CacheDecorator implements LazyDecorator<any, CacheOptions> {
constructor(private readonly cache: Cache) {}
wrap({ method, metadata: options }: WrapParams<any, CacheOptions>) {
return (...args: any[]) => {
const cached = this.cache.get(...args);
return cached ?? this.cache.set(method(...args), ...args);
};
}
}
@Injectable()
export class UserService {
@Cache({ ttl: 1000 })
findAll() {
// ...
}
}See Usage below for the full, step-by-step breakdown of how the pieces fit together.
Usage
1. Import AopModule
@Module({
imports: [
// ...
AopModule,
],
})
export class AppModule {}2. Create symbol for LazyDecorator
export const CACHE_DECORATOR = Symbol('CACHE_DECORATOR');3. Implement LazyDecorator using nestjs provider
metadata is passed as the second argument to createDecorator and is made available in the WrapParams for the wrap method.
@Aspect(CACHE_DECORATOR)
export class CacheDecorator implements LazyDecorator<any, CacheOptions> {
constructor(private readonly cache: Cache) {}
wrap({ method, metadata: options }: WrapParams<any, CacheOptions>) {
return (...args: any) => {
let cachedValue = this.cache.get(...args);
if (!cachedValue) {
cachedValue = method(...args);
this.cache.set(cachedValue, ...args);
}
return cachedValue;
};
}
}4. Add LazyDecoratorImpl to providers of module
@Module({
providers: [CacheDecorator],
})
export class CacheModule {}5. Create decorator that marks metadata of LazyDecorator
options can be obtained from the wrap method and used.
export const Cache = (options: CacheOptions) => createDecorator(CACHE_DECORATOR, options);6. Use it!
export class SomeService {
@Cache({
// ...options(metadata value)
})
some() {
// ...
}
}Getter, Setter and Inheritance Support
createDecorator can also be applied to get/set accessors, and decorated methods or accessors are inherited correctly by subclasses.
class UserService {
private _name = 'John';
@Cache({ ttl: 1000 })
get name() {
return this._name.toUpperCase();
}
}- A decorator can be applied to a
getaccessor, asetaccessor, or a regular method — but not to a property that has both a getter and a setter. Split them into separate properties, or use a regular method instead. - A decorated method or accessor keeps working when called through a subclass instance.
Caveats
If you’re testing with NestJS’s TestingModule, don’t forget to call the init method.
import { Test } from '@nestjs/testing';
const module = await Test.createTestingModule({
// ...
}).compile();
await module.init();References
- https://toss.tech/article/nestjs-custom-decorator
- https://youtu.be/VH1GTGIMHQw?t=2973
Contributing
We welcome contributions from everyone to this project. Read CONTRIBUTING.md for detailed contribution guide.
License
MIT © Viva Republica, Inc. See LICENSE for details.
