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
@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);
}
}
@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);
}
}
}
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);
}
}
}
@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
Difficult implementation
Lack of standard
Experimental decorators
The framework
Errors handling
Business logic in aspects
@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
@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
@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