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
npm install nestjs-saop
# or
yarn add nestjs-saop
# or
pnpm add nestjs-saop
import { AOPModule } from 'nestjs-saop';
@Module({
imports: [
// ... other modules
AOPModule.forRoot(),
],
})
export class AppModule {}
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);
};
}
}
import { LoggingDecorator } from './logging.decorator';
@Module({
providers: [LoggingDecorator],
})
export class AppModule {}
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<User> {
return await this.userRepository.findById(id);
}
@PerformanceDecorator.around({ logPerformance: true, threshold: 1000 })
async expensiveOperation(): Promise<any> {
await new Promise(resolve => setTimeout(resolve, 500));
return { result: 'done' };
}
}
π Around
βΆοΈ Before
β AfterReturning
orβ AfterThrowing
βΉοΈ After
π Around
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.
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).
Use case: Complete control over method execution, perfect for caching, performance monitoring, or transaction management.
@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<User> {
return await this.userRepository.findById(id);
}
}
Use case: Logging method calls, validation, authentication checks.
@Aspect()
export class LoggingDecorator extends AOPDecorator {
before({ method, options }) {
return (...args: any[]) => {
console.log(`βΆοΈ [${new Date().toISOString()}] ${method.name} called with:`, args);
};
}
}
// Usage
@Injectable()
export class PaymentService {
@LoggingDecorator.before({ level: 'info' })
async processPayment(amount: number, userId: string): Promise<PaymentResult> {
return { success: true, transactionId: 'tx_123' };
}
}
Use case: Cleanup operations, resource management, regardless of method success/failure.
@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<void> {
const fileHandle = await fs.open(filePath, 'r');
try {
await this.processFileContent(fileHandle);
} finally {
await fileHandle.close();
}
}
}
Use case: Post-processing successful results, response formatting, metrics collection.
@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<UserData> {
return await this.userRepository.findById(userId);
}
}
Use case: Error logging, error recovery, fallback mechanisms.
@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<ExternalData> {
const response = await fetch(endpoint);
if (!response.ok) {
throw new Error(`API call failed: ${response.status}`);
}
return response.json();
}
}
The AOPDecorator
class uses TypeScript generics to provide strong typing and better IntelliSense support:
Usage Examples:
// 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<LoggingOptions>) {
return (...args: any[]) => {
const timestamp = options.includeTimestamp ? `[${new Date().toISOString()}] ` : '';
console.log(`${timestamp}${options.level.toUpperCase()}: ${method.name} called`);
};
}
}
// With return type and error type
interface ApiResponse<T> {
success: boolean;
data: T;
error?: string;
}
@Aspect()
export class ApiDecorator extends AOPDecorator {
// `AOPOptions` here is the basic option type.
afterReturning({ method, options, result }: ResultAOPContext<AOPOptions, ApiResponse<any>>) {
return (...args: any[]) => {
console.log(`β
API call successful: ${method.name}`);
// result is typed as ApiResponse<any>
if (result.success) {
console.log(`π Response data:`, result.data);
}
};
}
// `AOPOptions` here is the basic option type.
afterThrowing({ method, options, error }: ErrorAOPContext<AOPOptions, Error>) {
return (...args: any[]) => {
console.error(`β API call failed: ${method.name}`, 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<User> {
// Method implementation
}
@ApiDecorator.afterReturning()
async getUserData(id: string): Promise<ApiResponse<User>> {
// Method implementation
}
}
Benefits of Using Generics:
- Type Safety: Catch type errors at compile time
- Better IntelliSense: IDE provides accurate autocompletion
- Self-Documenting Code: Types serve as documentation
Context Types by Advice Type:
// Before, After advice
UnitAOPContext<Options> = {
method: Function;
options: Options;
}
// AfterReturning advice
ResultAOPContext<Options, ReturnType> = {
method: Function;
options: Options;
result: ReturnType; // Available only in afterReturning
}
// Around advice
AroundAOPContext<Options> = {
method: Function;
instance: object;
proceed: Function;
options: Options;
};
// AfterThrowing advice
ErrorAOPContext<Options, ErrorType> = {
method: Function;
options: Options;
error: ErrorType; // Available only in afterThrowing
}
@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<ComplexResult> {
// 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);
}
}
The AOPModule.forRoot
method configures the AOPModule
as a global module. However, you can also import the AOPModule
into specific modules if needed.
@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.
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.