npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

nestjs-saop

v0.4.1

Published

Spring style AOP in Nest.js

Downloads

68

Readme

nestjs-saop

npm version codecov Github Workflow package license

English | 한국어

Spring style AOP (Aspect Oriented Programming) in Nest.js

Features

  • 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

Installation

npm install nestjs-saop
# or
yarn add nestjs-saop
# or
pnpm add nestjs-saop

Quick Start

1. Import AOPModule

import { AOPModule } from 'nestjs-saop';

@Module({
  imports: [
    // ... other modules
    AOPModule.forRoot(),
  ],
})
export class AppModule {}

2. Create AOP Decorator Implementation

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);
    };
  }
}

3. Register Decorator in Module

import { LoggingDecorator } from './logging.decorator';

@Module({
  providers: [LoggingDecorator],
})
export class AppModule {}

4. Use AOP Decorators

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' };
  }
}

Usage Guide

AOP execution cycle

  1. 🔄 Around
  2. ▶️ Before
  3. ✅ AfterReturning or ❌ AfterThrowing
  4. ⏹️ After
  5. 🔄 Around

AOP Execution Order

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).

AOP Advice Types

Around Advice

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);
  }
}

Before Advice

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' };
  }
}

After Advice

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();
    }
  }
}

AfterReturning Advice

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);
  }
}

AfterThrowing Advice

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();
  }
}

Configuration Options

AOPDecorator Generics

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:

  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:

// 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
}

Multiple Decorators on Single Method

@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);
  }
}

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.

@Injectable()
@LoggingDecorator.around()
class HelloService {
  getHello(name: string) {
    return `Hello ${name}!`;
  }
}

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.

@Module({
  imports: [AOPModule],
})
export class SpecificModule {}

Testing AOP Decorators

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');
  });
});

Contributing

We welcome contributions! Please see our Contributing Guide for details.