Spring style AOP in Nest.js
npm install nestjs-saop

!Github Workflow

English | 한국어
Spring style AOP (Aspect Oriented Programming) in Nest.js
- ✅ Complete AOP Advice Types: Support for all 5 Spring-style AOP advice types
- Around: Complete control over method execution (before, during, and after)
- Before: Execute advice before method invocation
- After: Execute advice after method completion (regardless of success/failure)
- AfterReturning: Execute advice only when method completes successfully
- AfterThrowing: Execute advice only when method throws an exception
- ✅ Full TypeScript Support: Complete type safety with generics and interfaces
- Strongly typed AOP contexts and options
- Generic support for method return types and error types
- IntelliSense support for all AOP operations
- ✅ NestJS Integration: Seamless integration with NestJS module system
- AOPModule.forRoot() for global AOP configuration
- Automatic instance discovery using NestJS DiscoveryModule
- Compatible with all NestJS dependency injection patterns
- ✅ Flexible Configuration: Highly configurable AOP options and contexts
- Conditional AOP execution based on runtime conditions
- Multiple decorators per method with different configurations
- ✅ Decorator Pattern Implementation: Clean decorator-based API
- @Aspect({ order?: number }) decorator for AOP class identification with optional execution order control
- Static method decorators for easy application
``bash`
npm install nestjs-saopor
yarn add nestjs-saopor
pnpm add nestjs-saop
`ts
import { AOPModule } from 'nestjs-saop';
@Module({
imports: [
// ... other modules
AOPModule.forRoot(),
],
})
export class AppModule {}
`
`ts
import { AOPDecorator, Aspect } from 'nestjs-saop';
@Aspect()
export class LoggingDecorator extends AOPDecorator {
around({ method, proceed, options }) {
return (...args: any[]) => {
console.log('🔄 Around: Before method call', ...args);
const result = proceed(...args);
console.log('🔄 Around: After method call', result);
return result;
};
}
before({ method, options }) {
return (...args: any[]) => {
console.log('▶️ Before: Method called with', ...args);
};
}
after({ method, options }) {
return (...args: any[]) => {
console.log('⏹️ After: Method completed');
};
}
afterReturning({ method, options, result }) {
return (...args: any[]) => {
console.log('✅ AfterReturning: Method returned', result);
};
}
afterThrowing({ method, options, error }): (...args: any[]) => void {
return (...args: any[]) => {
console.log('❌ AfterThrowing: Method threw', error.message);
};
}
}
`
`ts
import { LoggingDecorator } from './logging.decorator';
@Module({
providers: [LoggingDecorator],
})
export class AppModule {}
`
`ts
import { LoggingDecorator, CachingDecorator, PerformanceDecorator } from 'example-path';
@Injectable()
export class ExampleService {
@LoggingDecorator.after({ level: 'info', logArgs: true, logResult: true })
processData(data: any): string {
return Processed: ${data};
}
@CachingDecorator.afterReturn({ ttl: 300000 })
async getUserById(id: string): Promise
return await this.userRepository.findById(id);
}
@PerformanceDecorator.around({ logPerformance: true, threshold: 1000 })
async expensiveOperation(): Promise
await new Promise(resolve => setTimeout(resolve, 500));
return { result: 'done' };
}
}
`
1. 🔄 Around▶️ Before
2. ✅ AfterReturning
3. or ❌ AfterThrowing⏹️ After
4. 🔄 Around
5.
When multiple AOP decorators are applied to the same method, you can control the execution order using the order option in the @Aspect() decorator. Lower order values execute first. If no order is specified, the default is Number.MAX_SAFE_INTEGER, giving it the lowest priority.
`ts
import { AOPDecorator, Aspect } from 'nestjs-saop';
class AOPTracker {
static executionOrder: string[] = [];
static reset() {
this.executionOrder = [];
}
}
@Aspect({ order: 1 })
class FirstAOP extends AOPDecorator {
before() {
return () => {
AOPTracker.executionOrder.push('First');
};
}
}
@Aspect({ order: 2 })
class SecondAOP extends AOPDecorator {
before() {
return () => {
AOPTracker.executionOrder.push('Second');
};
}
}
@Aspect({ order: 3 })
class ThirdAOP extends AOPDecorator {
before() {
return () => {
AOPTracker.executionOrder.push('Third');
};
}
}
@Injectable()
class TestService {
@FirstAOP.before()
@SecondAOP.before()
@ThirdAOP.before()
getOrdered(): string {
return 'Ordered AOP executed';
}
}
`
In this example, when getOrdered() is called, the AOPs will execute in order: First (order 1), Second (order 2), Third (order 3).
#### Around Advice
Use case: Complete control over method execution, perfect for caching, performance monitoring, or transaction management.
`ts
@Aspect()
export class CachingDecorator extends AOPDecorator {
private cache = new Map();
around({ method, options, proceed }) {
return (...args: any[]) => {
const key = ${method.name}:${JSON.stringify(args)};
if (this.cache.has(key)) {
console.log('🔄 Cache hit!');
return this.cache.get(key);
}
console.log('🔄 Cache miss, executing method...');
const result = proceed(...args);
if (options.ttl) {
setTimeout(() => this.cache.delete(key), options.ttl);
}
this.cache.set(key, result);
return result;
};
}
}
// Usage
@Injectable()
export class UserService {
@CachingDecorator.around({ ttl: 300000 })
async getUserById(id: string): Promise
return await this.userRepository.findById(id);
}
}
`
#### Before Advice
Use case: Logging method calls, validation, authentication checks.
`ts▶️ [${new Date().toISOString()}] ${method.name} called with:
@Aspect()
export class LoggingDecorator extends AOPDecorator {
before({ method, options }) {
return (...args: any[]) => {
console.log(, args);
};
}
}
// Usage
@Injectable()
export class PaymentService {
@LoggingDecorator.before({ level: 'info' })
async processPayment(amount: number, userId: string): Promise
return { success: true, transactionId: 'tx_123' };
}
}
`
#### After Advice
Use case: Cleanup operations, resource management, regardless of method success/failure.
`ts
@Aspect()
export class ResourceCleanupDecorator extends AOPDecorator {
after({ method, options }) {
return (...args: any[]) => {
console.log('🧹 Cleaning up resources after method execution');
// Cleanup logic here
};
}
}
// Usage
@Injectable()
export class FileService {
@ResourceCleanupDecorator.after()
async processFile(filePath: string): Promise
const fileHandle = await fs.open(filePath, 'r');
try {
await this.processFileContent(fileHandle);
} finally {
await fileHandle.close();
}
}
}
`
#### AfterReturning Advice
Use case: Post-processing successful results, response formatting, metrics collection.
`ts
@Aspect()
export class ResponseFormatterDecorator extends AOPDecorator {
afterReturning({ method, options, result }) {
return (...args: any[]) => {
console.log('✅ Method completed successfully');
if (options.format === 'json') {
return {
success: true,
data: result,
timestamp: new Date().toISOString()
};
}
return result;
};
}
}
// Usage
@Injectable()
export class ApiService {
@ResponseFormatterDecorator.afterReturning({ format: 'json' })
async getUserData(userId: string): Promise
return await this.userRepository.findById(userId);
}
}
`
#### AfterThrowing Advice
Use case: Error logging, error recovery, fallback mechanisms.
`ts
@Aspect()
export class ErrorHandlingDecorator extends AOPDecorator {
constructor(private readonly errorLogger: ErrorLogger) {}
afterThrowing({ method, options, error }) {
return (...args: any[]) => {
console.error(❌ Method ${method.name} failed:, error.message);
if (options.retry && options.retryCount < 3) {
console.log(🔄 Retrying... (${options.retryCount + 1}/3));
// Implement retry logic
}
// Log to external service
this.errorLogger.log({
method: method.name,
error: error.message,
timestamp: new Date().toISOString(),
args: options.logArgs ? args : undefined
});
};
}
}
// Usage
@Injectable()
export class ExternalApiService {
@ErrorHandlingDecorator.afterThrowing({ retry: true, retryCount: 0, logArgs: true })
async callExternalAPI(endpoint: string): Promise
const response = await fetch(endpoint);
if (!response.ok) {
throw new Error(API call failed: ${response.status});`
}
return response.json();
}
}
#### AOPDecorator Generics
The AOPDecorator class uses TypeScript generics to provide strong typing and better IntelliSense support:
Usage Examples:
`ts
// Basic usage with default generics
@Aspect()
export class BasicDecorator extends AOPDecorator {
// Options = AOPOptions (default type)
}
// With custom options type
interface LoggingOptions {
level: 'debug' | 'info' | 'warn' | 'error';
includeTimestamp: boolean;
}
@Aspect()
export class LoggingDecorator extends AOPDecorator {
// Generic type parameter for custom options
// This enables TypeScript to infer the option type when using LoggingDecorator.before()
before({ method, options }: UnitAOPContext
return (...args: any[]) => {
const timestamp = options.includeTimestamp ? [${new Date().toISOString()}] : '';${timestamp}${options.level.toUpperCase()}: ${method.name} called
console.log();
};
}
}
// With return type and error type
interface ApiResponse
success: boolean;
data: T;
error?: string;
}
@Aspect()
export class ApiDecorator extends AOPDecorator {
// AOPOptions here is the basic option type.✅ API call successful: ${method.name}
afterReturning({ method, options, result }: ResultAOPContext
return (...args: any[]) => {
console.log();📊 Response data:
// result is typed as ApiResponse
if (result.success) {
console.log(, result.data);
}
};
}
// AOPOptions here is the basic option type.❌ API call failed: ${method.name}
afterThrowing({ method, options, error }: ErrorAOPContext
return (...args: any[]) => {
console.error(, error.message);
// error is typed as Error
};
}
}
// Usage with typed decorators
@Injectable()
export class UserService {
@LoggingDecorator.before({
level: 'info',
includeTimestamp: true
})
async getUser(id: string): Promise
// Method implementation
}
@ApiDecorator.afterReturning()
async getUserData(id: string): Promise
// Method implementation
}
}
`
Benefits of Using Generics:
1. Type Safety: Catch type errors at compile time
2. Better IntelliSense: IDE provides accurate autocompletion
3. Self-Documenting Code: Types serve as documentation
Context Types by Advice Type:
`ts
// Before, After advice
UnitAOPContext
method: Function;
options: Options;
}
// AfterReturning advice
ResultAOPContext
method: Function;
options: Options;
result: ReturnType; // Available only in afterReturning
}
// Around advice
AroundAOPContext
method: Function;
instance: object;
proceed: Function;
options: Options;
};
// AfterThrowing advice
ErrorAOPContext
method: Function;
options: Options;
error: ErrorType; // Available only in afterThrowing
}
`
#### Multiple Decorators on Single Method
`ts`
@Injectable()
export class ComplexService {
@LoggingDecorator.before({ level: 'info', logArgs: true })
@PerformanceDecorator.around({ threshold: 1000, logPerformance: true })
@CachingDecorator.around({ ttl: 300000 })
@ErrorHandlingDecorator.afterThrowing({ retry: true, logArgs: true })
async complexOperation(data: ComplexData): Promise
// Method will be enhanced with:
// 1. Performance monitoring around execution
// 2. Logging before execution
// 3. Error handling if something goes wrong
// 4. Caching around execution
return await this.processComplexData(data);
}
}
#### Class-level AOP Decorator
When you declare an AOP decorator at the class level, it applies to all public methods of the class.
> [!WARNING]
> Class-level AOP decorators cannot be applied to private methods, getters, setters, static methods, or arrow functions.
> They also do not apply to inherited methods. This is intentional behavior.
`tsHello ${name}!
@Injectable()
@LoggingDecorator.around()
class HelloService {
getHello(name: string) {
return ;`
}
}
#### Importing AOPModule
The AOPModule.forRoot method configures the AOPModule as a global module. However, you can also import the AOPModule into specific modules if needed.`ts`
@Module({
imports: [AOPModule],
})
export class SpecificModule {}
When testing with NestJS's TestingModule, ensure that you call the init() method to properly initialize the AOP system.
`ts
describe('AOP Integration (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AOPModule.forRoot()],
providers: [LoggingDecorator, TestService],
}).compile();
app = moduleFixture.createNestApplication();
await app.init(); // Required for AOP initialization
});
it('should apply AOP advice to service methods', () => {
const testService = app.get(TestService);
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
const result = testService.testMethod('test');
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Before: Method called')
);
expect(result).toBe('processed: test');
});
});
``
We welcome contributions! Please see our Contributing Guide for details.