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;
  }
}
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;
      }
    };
  };
@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);
    }
  }
}
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 {
  // ...
  
  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;
      }
    };
  }
}
@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 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,
  ) {}

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

maciej-sikorski-a01b26149

@_MaciejSikorski

bottega.com.pl/szkolenie-nestjs

Sikora00/aop

nestjs-talks.com

Q&A

Made with Slides.com