Aspect-Oriented Programming
with NestJS
What is Aspect-oriented programming?


-
Interceptors
have a set of useful capabilities which are inspired by the Aspect Oriented Programming (AOP) technique. -
Guards (Auth)
-
Pipes (Validation)
-
Other decorators
NestJS and AOP - The Necessary

Classic use case for interceptors
@Controller('cats') export class CatsController { private readonly logger = new Logger('CatsController'); constructor(private readonly catsService: CatsService) {} @Get() async findAll(): Promise<Cat[]> { this.logger.log('GET /cats requested'); const now = new Date(); const result = await this.catsService.findAll(); this.logger.log(`GET /cats completed in ${new Date() - now}ms`); return result; } @Post() async create(Body() dto: CreateCatDto): Promise<Cat> { this.logger.log('POST /cats requested'); const now = new Date(); const result = await this.catsService.create(dto); this.logger.log(`POST /cats completed in ${new Date() - now}ms`); return result; } }
@Controller('cats') export class CatsController { private readonly logger = new Logger('CatsController'); constructor(private readonly catsService: CatsService) {} @Get() async findAll(): Promise<Cat[]> { this.logger.log('GET /cats requested'); const now = new Date(); const result = await this.catsService.findAll(); this.logger.log(`GET /cats completed in ${new Date() - now}ms`); return result; } @Post() async create(Body() dto: CreateCatDto): Promise<Cat> { this.logger.log('POST /cats requested'); const now = new Date(); const result = await this.catsService.create(dto); this.logger.log(`POST /cats completed in ${new Date() - now}ms`); return result; } }
export class LoggingInterceptor implements NestInterceptor { private readonly logger = new Logger('LoggingInterceptor'); intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const request = context.switchToHttp().getRequest(); const endpoint = `${request.method} ${request.url}`; this.logger.log(`${endpoint} requested`); const now = new Date(); return next .handle() .pipe( tap(() => this.logger.log(`${endpoint} completed in ${new Date() - now}ms`)) ); } }
export class LoggingInterceptor implements NestInterceptor { private readonly logger = new Logger('LoggingInterceptor'); intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const request = context.switchToHttp().getRequest(); const endpoint = `${request.method} ${request.url}`; this.logger.log(`${endpoint} requested`); const now = new Date(); return next .handle() .pipe( tap(() => this.logger.log(`${endpoint} completed in ${new Date() - now}ms`)) ); } }
export class LoggingInterceptor implements NestInterceptor { private readonly logger = new Logger('LoggingInterceptor'); intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const request = context.switchToHttp().getRequest(); const endpoint = `${request.method} ${request.url}`; this.logger.log(`${endpoint} requested`); const now = new Date(); return next .handle() .pipe( tap(() => this.logger.log(`${endpoint} completed in ${new Date() - now}ms`)) ); } }
export class LoggingInterceptor implements NestInterceptor { private readonly logger = new Logger('LoggingInterceptor'); intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const request = context.switchToHttp().getRequest(); const endpoint = `${request.method} ${request.url}`; this.logger.log(`${endpoint} requested`); const now = new Date(); return next .handle() .pipe( tap(() => this.logger.log(`${endpoint} completed in ${new Date() - now}ms`)) ); } }
export class LoggingInterceptor implements NestInterceptor { private readonly logger = new Logger('LoggingInterceptor'); intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const request = context.switchToHttp().getRequest(); const endpoint = `${request.method} ${request.url}`; this.logger.log(`${endpoint} requested`); const now = new Date(); return next .handle() .pipe( tap(() => this.logger.log(`${endpoint} completed in ${new Date() - now}ms`)) ); } }
@Controller('cats') export class CatsController { private readonly logger = new Logger('CatsController'); constructor(private readonly catsService: CatsService) {} @Get() async findAll(): Promise<Cat[]> { this.logger.log('GET /cats requested'); const now = new Date(); const result = await this.catsService.findAll(); this.logger.log(`GET /cats completed in ${new Date() - now}ms`); return result; } @Post() async create(Body() dto: CreateCatDto): Promise<Cat> { this.logger.log('POST /cats requested'); const now = new Date(); const result = await this.catsService.create(dto); this.logger.log(`POST /cats completed in ${new Date() - now}ms`); return result; } }
@Controller('cats') export class CatsController { constructor(private readonly catsService: CatsService) {} @Get() async findAll(): Promise<Cat[]> { return this.catsService.findAll(); } @Post() async create(Body() dto: CreateCatDto): Promise<Cat> { return this.catsService.create(dto); } }
@UseInterceptors(LoggingInterceptor) @Controller('cats') export class CatsController { constructor(private readonly catsService: CatsService) {} @Get() async findAll(): Promise<Cat[]> { return this.catsService.findAll(); } @Post() async create(Body() dto: CreateCatDto): Promise<Cat> { return this.catsService.create(dto); } }

Is that all?
How to write our own aspects?
@Injectable() export class AdoptService { private readonly logger = new Logger('Adopt'); constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly connection: Connection, private readonly eventPublisher: EventPublisher, private readonly datadog: Datadog, private readonly metrics: Metrics, ) {} async adopt(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { try { this.logger.log({ message: 'Adoption started', data: { ...dto, requestId }, }); this.metrics.begin(requestId); const adoption = await this.connection.transaction( async (transaction) => { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); return this.adoptionRepository.create(adoption, transaction); }, ); adoption.commit(); this.logger.log({ message: 'Adoption completed', data: { ...dto, requestId }, }); } catch (e) { this.logger.error({ message: 'Adoption failed', error: e, data: { ...dto, requestId }, }); this.datadog.registerError(e); throw e; } finally { this.metrics.finish(requestId); } } }
@Injectable() export class AdoptService { private readonly logger = new Logger('Adopt'); constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly connection: Connection, private readonly eventPublisher: EventPublisher, private readonly datadog: Datadog, private readonly metrics: Metrics, ) {} async adopt(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { try { this.logger.log({ message: 'Adoption started', data: { ...dto, requestId }, }); this.metrics.begin(requestId); const adoption = await this.connection.transaction( async (transaction) => { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); return this.adoptionRepository.create(adoption, transaction); }, ); adoption.commit(); this.logger.log({ message: 'Adoption completed', data: { ...dto, requestId }, }); } catch (e) { this.logger.error({ message: 'Adoption failed', error: e, data: { ...dto, requestId }, }); this.datadog.registerError(e); throw e; } finally { this.metrics.finish(requestId); } } }
@Injectable() export class AdoptService { private readonly logger = new Logger('Adopt'); constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly connection: Connection, private readonly eventPublisher: EventPublisher, private readonly datadog: Datadog, private readonly metrics: Metrics, ) {} async adopt(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { try { this.logger.log({ message: 'Adoption started', data: { ...dto, requestId }, }); this.metrics.begin(requestId); const adoption = await this.connection.transaction( async (transaction) => { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); return this.adoptionRepository.create(adoption, transaction); }, ); adoption.commit(); this.logger.log({ message: 'Adoption completed', data: { ...dto, requestId }, }); } catch (e) { this.logger.error({ message: 'Adoption failed', error: e, data: { ...dto, requestId }, }); this.datadog.registerError(e); throw e; } finally { this.metrics.finish(requestId); } } }
@Injectable() export class AdoptService { private readonly logger = new Logger('Adopt'); constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly connection: Connection, private readonly eventPublisher: EventPublisher, private readonly datadog: Datadog, private readonly metrics: Metrics, ) {} async adopt(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { try { this.logger.log({ message: 'Adoption started', data: { ...dto, requestId }, }); this.metrics.begin(requestId); const adoption = await this.connection.transaction( async (transaction) => { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); return this.adoptionRepository.create(adoption, transaction); }, ); adoption.commit(); this.logger.log({ message: 'Adoption completed', data: { ...dto, requestId }, }); } catch (e) { this.logger.error({ message: 'Adoption failed', error: e, data: { ...dto, requestId }, }); this.datadog.registerError(e); throw e; } finally { this.metrics.finish(requestId); } } }
@Injectable() export class AdoptService { private readonly logger = new Logger('Adopt'); constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly connection: Connection, private readonly eventPublisher: EventPublisher, private readonly datadog: Datadog, private readonly metrics: Metrics, ) {} async adopt(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { try { this.logger.log({ message: 'Adoption started', data: { ...dto, requestId }, }); this.metrics.begin(requestId); const adoption = await this.connection.transaction( async (transaction) => { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); return this.adoptionRepository.create(adoption, transaction); }, ); adoption.commit(); this.logger.log({ message: 'Adoption completed', data: { ...dto, requestId }, }); } catch (e) { this.logger.error({ message: 'Adoption failed', error: e, data: { ...dto, requestId }, }); this.datadog.registerError(e); throw e; } finally { this.metrics.finish(requestId); } } }
The logic that doesn’t require the framework
import { Logger } from '@nestjs/common'; export const LogAroundDecorator = (action: string) => (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => { const logger = new Logger(); const originalMethod = descriptor.value; descriptor.value = async function (...args: unknown[]) { try { logger.log({ message: `${action} started`, data: args }); const result = await originalMethod.apply(this, args); logger.log({ message: `${action} completed`, data: args }); return result; } catch (e) { logger.error({ message: `${action} failed`, error: e, data: args }); throw e; } }; };
import { Logger } from '@nestjs/common'; export const LogAroundDecorator = (action: string) => (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => { const logger = new Logger(); const originalMethod = descriptor.value; descriptor.value = async function (...args: unknown[]) { try { logger.log({ message: `${action} started`, data: args }); const result = await originalMethod.apply(this, args); logger.log({ message: `${action} completed`, data: args }); return result; } catch (e) { logger.error({ message: `${action} failed`, error: e, data: args }); throw e; } }; };
import { Logger } from '@nestjs/common'; export const LogAroundDecorator = (action: string) => (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => { const logger = new Logger(); const originalMethod = descriptor.value; descriptor.value = async function (...args: unknown[]) { try { logger.log({ message: `${action} started`, data: args }); const result = await originalMethod.apply(this, args); logger.log({ message: `${action} completed`, data: args }); return result; } catch (e) { logger.error({ message: `${action} failed`, error: e, data: args }); throw e; } }; };
import { Logger } from '@nestjs/common'; export const LogAroundDecorator = (action: string) => (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => { const logger = new Logger(); const originalMethod = descriptor.value; descriptor.value = async function (...args: unknown[]) { try { logger.log({ message: `${action} started`, data: args }); const result = await originalMethod.apply(this, args); logger.log({ message: `${action} completed`, data: args }); return result; } catch (e) { logger.error({ message: `${action} failed`, error: e, data: args }); throw e; } }; };
import { Logger } from '@nestjs/common'; export const LogAroundDecorator = (action: string) => (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => { const logger = new Logger(); const originalMethod = descriptor.value; descriptor.value = async function (...args: unknown[]) { try { logger.log({ message: `${action} started`, data: args }); const result = await originalMethod.apply(this, args); logger.log({ message: `${action} completed`, data: args }); return result; } catch (e) { logger.error({ message: `${action} failed`, error: e, data: args }); throw e; } }; };
@Injectable() export class AdoptService { private readonly logger = new Logger('Adopt'); constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly connection: Connection, private readonly eventPublisher: EventPublisher, private readonly datadog: Datadog, private readonly metrics: Metrics, ) {} async adopt(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { try { this.logger.log({ message: 'Adoption started', data: { ...dto, requestId }, }); this.metrics.begin(requestId); const adoption = await this.connection.transaction( async (transaction) => { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); return this.adoptionRepository.create(adoption, transaction); }, ); adoption.commit(); this.logger.log({ message: 'Adoption completed', data: { ...dto, requestId }, }); } catch (e) { this.logger.error({ message: 'Adoption failed', error: e, data: { ...dto, requestId }, }); this.datadog.registerError(e); throw e; } finally { this.metrics.finish(requestId); } } }
@Injectable() export class AdoptService { constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly connection: Connection, private readonly eventPublisher: EventPublisher, private readonly datadog: Datadog, private readonly metrics: Metrics, ) {} async adopt(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { try { this.metrics.begin(requestId); const adoption = await this.connection.transaction( async (transaction) => { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); return this.adoptionRepository.create(adoption, transaction); }, ); adoption.commit(); } catch (e) { this.datadog.registerError(e); throw e; } finally { this.metrics.finish(requestId); } } }
@Injectable() export class AdoptService { constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly connection: Connection, private readonly eventPublisher: EventPublisher, private readonly datadog: Datadog, private readonly metrics: Metrics, ) {} async adopt(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { try { this.metrics.begin(requestId); const adoption = await this.connection.transaction( async (transaction) => { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); return this.adoptionRepository.create(adoption, transaction); }, ); adoption.commit(); } catch (e) { this.datadog.registerError(e); throw e; } finally { this.metrics.finish(requestId); } } }
@Injectable() export class AdoptService { constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly connection: Connection, private readonly eventPublisher: EventPublisher, private readonly datadog: Datadog, private readonly metrics: Metrics, ) {} @LogAroundDecorator('Adoption') async adopt(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { try { this.metrics.begin(requestId); const adoption = await this.connection.transaction( async (transaction) => { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); return this.adoptionRepository.create(adoption, transaction); }, ); adoption.commit(); } catch (e) { this.datadog.registerError(e); throw e; } finally { this.metrics.finish(requestId); } } }
Logic that requires the framework
@Injectable() export class AdoptService { constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly connection: Connection, private readonly eventPublisher: EventPublisher, private readonly datadog: Datadog, private readonly metrics: Metrics, ) {} @LogAroundDecorator('Adoption') async adopt(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { try { this.metrics.begin(requestId); const adoption = await this.connection.transaction( async (transaction) => { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); return this.adoptionRepository.create(adoption, transaction); }, ); adoption.commit(); } catch (e) { this.datadog.registerError(e); throw e; } finally { this.metrics.finish(requestId); } } }
@Injectable() export class AdoptService { constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly connection: Connection, private readonly eventPublisher: EventPublisher, private readonly datadog: Datadog, private readonly metrics: Metrics, ) {} @LogAroundDecorator('Adoption') async adopt(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { try { this.metrics.begin(requestId); const adoption = await this.connection.transaction( async (transaction) => { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); return this.adoptionRepository.create(adoption, transaction); }, ); adoption.commit(); } catch (e) { this.datadog.registerError(e); throw e; } finally { this.metrics.finish(requestId); } } }
import { SetMetadata } from '@nestjs/common'; export const ERRORS_MONITORING_KEY = Symbol('ERRORS_MONITORING'); export const ErrorsMonitoring = SetMetadata(ERRORS_MONITORING_KEY, true);
export class ErrorsMonitoringExplorer implements OnModuleInit { onModuleInit(): void { this.explore(); } explore(): void { const instanceWrappers: InstanceWrapper[] = this.discoveryService.getProviders(); instanceWrappers.forEach((wrapper: InstanceWrapper) => { const { instance } = wrapper; if (!instance) { return; } // scanFromPrototype will iterate through all providers' methods this.metadataScanner.scanFromPrototype( instance, Object.getPrototypeOf(instance), (methodName: string) => this.lookupProviderMethod(instance, methodName), ); }); } // ... }
export class ErrorsMonitoringExplorer implements OnModuleInit { onModuleInit(): void { this.explore(); } explore(): void { const instanceWrappers: InstanceWrapper[] = this.discoveryService.getProviders(); instanceWrappers.forEach((wrapper: InstanceWrapper) => { const { instance } = wrapper; if (!instance) { return; } // scanFromPrototype will iterate through all providers' methods this.metadataScanner.scanFromPrototype( instance, Object.getPrototypeOf(instance), (methodName: string) => this.lookupProviderMethod(instance, methodName), ); }); } // ... }
export class ErrorsMonitoringExplorer implements OnModuleInit { onModuleInit(): void { this.explore(); } explore(): void { const instanceWrappers: InstanceWrapper[] = this.discoveryService.getProviders(); instanceWrappers.forEach((wrapper: InstanceWrapper) => { const { instance } = wrapper; if (!instance) { return; } // scanFromPrototype will iterate through all providers' methods this.metadataScanner.scanFromPrototype( instance, Object.getPrototypeOf(instance), (methodName: string) => this.lookupProviderMethod(instance, methodName), ); }); } // ... }
export class ErrorsMonitoringExplorer implements OnModuleInit { onModuleInit(): void { this.explore(); } explore(): void { const instanceWrappers: InstanceWrapper[] = this.discoveryService.getProviders(); instanceWrappers.forEach((wrapper: InstanceWrapper) => { const { instance } = wrapper; if (!instance) { return; } // scanFromPrototype will iterate through all providers' methods this.metadataScanner.scanFromPrototype( instance, Object.getPrototypeOf(instance), (methodName: string) => this.lookupProviderMethod(instance, methodName), ); }); } // ... }
export class ErrorsMonitoringExplorer implements OnModuleInit { // ... lookupProviderMethod( instance: Record<string, (arg: unknown) => Promise<void>>, methodName: string, ) { const methodRef = instance[methodName]; const isPointCutSet = this.reflector.get<string[]>( ERRORS_MONITORING_KEY, methodRef, ); if (!isPointCutSet) { return; } this.errorMonitoringProvider.attach(instance, methodName); } }
export class ErrorsMonitoringExplorer implements OnModuleInit { // ... lookupProviderMethod( instance: Record<string, (arg: unknown) => Promise<void>>, methodName: string, ) { const methodRef = instance[methodName]; const isPointCutSet = this.reflector.get<string[]>( ERRORS_MONITORING_KEY, methodRef, ); if (!isPointCutSet) { return; } this.errorMonitoringProvider.attach(instance, methodName); } }
export class ErrorsMonitoringExplorer implements OnModuleInit { // ... lookupProviderMethod( instance: Record<string, (arg: unknown) => Promise<void>>, methodName: string, ) { const methodRef = instance[methodName]; const isPointCutSet = this.reflector.get<string[]>( ERRORS_MONITORING_KEY, methodRef, ); if (!isPointCutSet) { return; } this.errorMonitoringProvider.attach(instance, methodName); } }
@Injectable() export class DatadogErrorMonitoringProvider implements ErrorMonitoringProvider { constructor(private readonly datadog: Datadog) {} attach( instance: Record<string, (...args: unknown[]) => Promise<void>>, methodName: string, ): void { const originalMethod = instance[methodName]; instance[methodName] = async (...args: unknown[]) => { try { return await originalMethod.apply(instance, args); } catch (e) { this.datadog.registerError(e); throw e; } }; } }
@Injectable() export class DatadogErrorMonitoringProvider implements ErrorMonitoringProvider { constructor(private readonly datadog: Datadog) {} attach( instance: Record<string, (...args: unknown[]) => Promise<void>>, methodName: string, ): void { const originalMethod = instance[methodName]; instance[methodName] = async (...args: unknown[]) => { try { return await originalMethod.apply(instance, args); } catch (e) { this.datadog.registerError(e); throw e; } }; } }
@Module({ imports: [DiscoveryModule, DatadogErrorMonitoringProviderModule], providers: [ErrorsMonitoringExplorer], }) export class ErrorsMonitoringModule {}
@Module({ imports: [ AdoptionModule, ...(process.env.reportErrors ? [ErrorsMonitoringModule] : []), ], }) export class AppModule {}
@Injectable() export class AdoptService { constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly connection: Connection, private readonly eventPublisher: EventPublisher, private readonly datadog: Datadog, private readonly metrics: Metrics, ) {} @LogAroundDecorator('Adoption') async adopt(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { try { this.metrics.begin(requestId); const adoption = await this.connection.transaction( async (transaction) => { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); return this.adoptionRepository.create(adoption, transaction); }, ); adoption.commit(); } catch (e) { this.datadog.registerError(e); throw e; } finally { this.metrics.finish(requestId); } } }
@Injectable() export class AdoptService { constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly connection: Connection, private readonly eventPublisher: EventPublisher, private readonly datadog: Datadog, private readonly metrics: Metrics, ) {} @LogAroundDecorator('Adoption') async adopt(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { try { this.metrics.begin(requestId); const adoption = await this.connection.transaction( async (transaction) => { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); return this.adoptionRepository.create(adoption, transaction); }, ); adoption.commit(); } catch (e) { this.datadog.registerError(e); throw e; } finally { this.metrics.finish(requestId); } } }
@Injectable() export class AdoptService { constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly connection: Connection, private readonly eventPublisher: EventPublisher, private readonly metrics: Metrics, ) {} @LogAroundDecorator('Adoption') async adopt(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { try { this.metrics.begin(requestId); const adoption = await this.connection.transaction( async (transaction) => { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); return this.adoptionRepository.create(adoption, transaction); }, ); adoption.commit(); } finally { this.metrics.finish(requestId); } } }
@Injectable() export class AdoptService { constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly connection: Connection, private readonly eventPublisher: EventPublisher, private readonly metrics: Metrics, ) {} @LogAroundDecorator('Adoption') async adopt(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { try { this.metrics.begin(requestId); const adoption = await this.connection.transaction( async (transaction) => { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); return this.adoptionRepository.create(adoption, transaction); }, ); adoption.commit(); } finally { this.metrics.finish(requestId); } } }
@Injectable() export class AdoptService { constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly connection: Connection, private readonly eventPublisher: EventPublisher, private readonly metrics: Metrics, ) {} @ErrorsMonitoring @LogAroundDecorator('Adoption') async adopt(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { try { this.metrics.begin(requestId); const adoption = await this.connection.transaction( async (transaction) => { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); return this.adoptionRepository.create(adoption, transaction); }, ); adoption.commit(); } finally { this.metrics.finish(requestId); } } }
@Injectable() export class AdoptService { constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly connection: Connection, private readonly eventPublisher: EventPublisher, private readonly metrics: Metrics, ) {} @ErrorsMonitoring @LogAroundDecorator('Adoption') async adopt(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { try { this.metrics.begin(requestId); const adoption = await this.connection.transaction( async (transaction) => { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); return this.adoptionRepository.create(adoption, transaction); }, ); adoption.commit(); } finally { this.metrics.finish(requestId); } } }
@Injectable() export class AdoptService { constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly connection: Connection, private readonly eventPublisher: EventPublisher, ) {} @ErrorsMonitoring @LogAroundDecorator('Adoption') async adopt(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { const adoption = await this.connection.transaction(async (transaction) => { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); return this.adoptionRepository.create(adoption, transaction); }); adoption.commit(); } }
@Injectable() export class AdoptService { constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly connection: Connection, private readonly eventPublisher: EventPublisher, ) {} @AttachMetrics @ErrorsMonitoring @LogAroundDecorator('Adoption') async adopt(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { const adoption = await this.connection.transaction(async (transaction) => { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); return this.adoptionRepository.create(adoption, transaction); }); adoption.commit(); } }
@Injectable() export class AdoptService { constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly connection: Connection, private readonly eventPublisher: EventPublisher, ) {} @AttachMetrics @ErrorsMonitoring @LogAroundDecorator('Adoption') async adopt(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { const adoption = await this.connection.transaction(async (transaction) => { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); return this.adoptionRepository.create(adoption, transaction); }); adoption.commit(); } }
@Injectable() export class AdoptService { constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly connection: Connection, private readonly eventPublisher: EventPublisher, ) {} @AttachMetrics @ErrorsMonitoring @LogAroundDecorator('Adoption') async adopt(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { const adoption = await this.connection.transaction(async (transaction) => { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); return this.adoptionRepository.create(adoption, transaction); }); adoption.commit(); } }
@UseCase('Adoption') export class AdoptService { constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly connection: Connection, private readonly eventPublisher: EventPublisher, ) {} async execute(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { const adoption = await this.connection.transaction(async (transaction) => { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); return this.adoptionRepository.create(adoption, transaction); }); adoption.commit(); } }
@UseCase('Adoption') export class AdoptService { constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly connection: Connection, private readonly eventPublisher: EventPublisher, ) {} async execute(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { const adoption = await this.connection.transaction(async (transaction) => { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); return this.adoptionRepository.create(adoption, transaction); }); adoption.commit(); } }







-
Simpler service
-
Feature toggle
-
Less boilerplate
-
Safer refactoring
-
Easier testing
The Good

-
Difficult implementation
-
Lack of standard
-
Experimental decorators
-
The framework
-
-
Errors handling
-
Business logic in aspects
The Bad

@Injectable() export class AdoptService { @Annotate('metrics') adopt() { // } } @Injectable() export class MetricsProvider { constructor(private readonly metrics: Metrics) {} @Before('metrics') startMetrics(ref) { ref.metricsId = uuid(); this.metrics.start(ref.metricsId); } @After('metrics') finishMetrics(ref) { this.metrics.finish(ref.metricsId); } }
@Injectable() export class AdoptService { @Annotate('metrics') adopt() { // } } @Injectable() export class MetricsProvider { constructor(private readonly metrics: Metrics) {} @Before('metrics') startMetrics(ref) { ref.metricsId = uuid(); this.metrics.start(ref.metricsId); } @After('metrics') finishMetrics(ref) { this.metrics.finish(ref.metricsId); } }
@Injectable() export class AdoptService { @Annotate('metrics') adopt() { // } } @Injectable() export class MetricsProvider { constructor(private readonly metrics: Metrics) {} @Before('metrics') startMetrics(ref) { ref.metricsId = uuid(); this.metrics.start(ref.metricsId); } @After('metrics') finishMetrics(ref) { this.metrics.finish(ref.metricsId); } }
-
Difficult implementation
-
Lack of standard
-
Experimental decorators
-
The framework
-
-
Errors handling
-
Business logic in aspects
The Bad

@Module({ imports: [ AdoptionModule, ErrorsMonitoringModule.optionallyForFeature([ AdoptService ]) ], }) export class AppModule {}
@Injectable() export class ErrorsMonitoringProvider { private static for = [AdoptService] attachMonitoring() { ErrorsMonitoringProvider.for.forEach( // ... ) } }
@Injectable() export class AdoptService implements OnModuleInit { constructor( private readonly errorMonitoringRegistry: ErrorMonitoringRegistry ) {} onModuleInit() { this.errorMonitoritngRegistry.attach(this) } }
-
Difficult implementation
-
Lack of standard
-
Experimental decorators
-
The framework
-
-
Errors handling
-
Business logic in aspects
The Bad

@Entity() export class User { @PrimaryGeneratedColumn('uuid') id: string; @OneToOne(() => UserActive) userActive: UserActive; @AfterInsert() public async handleAfterInsert() { const userActive = new UserActive(); userActive.token = randomString(); userActive.user = this; await getConnection().getRepository(UserActive).save(userActive); } }
@Injectable() export class AdoptService { constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly eventPublisher: EventPublisher, ) {} @Transactional() async adopt(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); this.adoptionRepository.create(adoption, transaction); adoption.commit(); } }
@Injectable() export class AdoptService { constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly eventPublisher: EventPublisher, ) {} @Transactional() async adopt(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); this.adoptionRepository.create(adoption, transaction); adoption.commit(); } }
@Injectable() export class AdoptService { constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly eventPublisher: EventPublisher, ) {} @Transactional() async adopt(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); this.adoptionRepository.create(adoption, transaction); adoption.commit(); } }
@Injectable() export class AdoptService { constructor( private readonly adoptionRepository: AdoptionRepository, private readonly adoptionRequestFactory: AdoptionRequestFactory, private readonly eventPublisher: EventPublisher, ) {} @Transactional() async adopt(dto: DataNeededForAdoption, requestId: RequestId): Promise<void> { const adoptionRequest = await this.adoptionRequestFactory.create( dto, transaction, ); const adoption = adoptionRequest.adopt(); this.eventPublisher.mergeObjectContext(adoption); this.adoptionRepository.create(adoption, transaction); adoption.commit(); } }
maciej-sikorski-a01b26149

@_MaciejSikorski

bottega.com.pl/szkolenie-nestjs

Sikora00/aop

nestjs-talks.com
Q&A
Aspect-Oriented Programming with NestJS
Aspect-oriented programming with NestJS
By Maciej Sikorski
Aspect-oriented programming with NestJS
- 68