typescript 59 lines · 7 steps

Decoupling side effects with NestJS events

Placing an order emits a single event, letting inventory, email, and logging react independently without the service knowing about them.

Explained by highlit
1import { Injectable, NotFoundException } from '@nestjs/common';
2import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
3import { InjectRepository } from '@nestjs/typeorm';
4import { Repository } from 'typeorm';
5import { Order } from './order.entity';
6import { MailerService } from '../mailer/mailer.service';
7import { InventoryService } from '../inventory/inventory.service';
8 
9export class OrderPlacedEvent {
10 constructor(
11 public readonly orderId: string,
12 public readonly customerEmail: string,
13 public readonly items: { sku: string; quantity: number }[],
14 ) {}
15}
16 
17@Injectable()
18export class OrdersService {
19 constructor(
20 @InjectRepository(Order) private readonly orders: Repository<Order>,
21 private readonly eventEmitter: EventEmitter2,
22 ) {}
23 
24 async placeOrder(customerEmail: string, items: { sku: string; quantity: number }[]) {
25 const order = await this.orders.save(
26 this.orders.create({ customerEmail, items, status: 'pending' }),
27 );
28 
29 this.eventEmitter.emit(
30 'order.placed',
31 new OrderPlacedEvent(order.id, customerEmail, items),
32 );
33 
34 return order;
35 }
36}
37 
38@Injectable()
39export class OrderNotificationsListener {
40 constructor(
41 private readonly mailer: MailerService,
42 private readonly inventory: InventoryService,
43 ) {}
44 
45 @OnEvent('order.placed', { async: true })
46 async onOrderPlaced(event: OrderPlacedEvent) {
47 await this.inventory.reserve(event.items);
48 await this.mailer.send({
49 to: event.customerEmail,
50 template: 'order-confirmation',
51 context: { orderId: event.orderId },
52 });
53 }
54 
55 @OnEvent('order.*')
56 logOrderActivity(event: OrderPlacedEvent) {
57 console.log(`[orders] activity for ${event.orderId}`);
58 }
59}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Emitting a domain event keeps the core write path unaware of downstream consumers, so adding reactions never touches the service.
  2. 2A typed event class carries exactly the data listeners need, avoiding refetches and keeping handlers self-contained.
  3. 3Wildcard subscriptions let cross-cutting concerns like logging hook into whole families of events at once.

Related explainers

typescript
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';

How a JWT strategy authenticates in NestJS

authentication jwt passport
Intermediate 7 steps
typescript
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http';
import { BehaviorSubject, Observable, throwError } from 'rxjs';
import { catchError, filter, take, switchMap } from 'rxjs/operators';

Refreshing auth tokens in an Angular interceptor

http-interceptor token-refresh rxjs
Advanced 8 steps
typescript
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { Reflector } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt';

How a GraphQL auth guard works in NestJS

authentication authorization jwt
Intermediate 7 steps
python
import os
from datetime import timedelta
 
 

Class-based config in a Flask app factory

configuration app-factory inheritance
Intermediate 7 steps
typescript
type ResizeCallback = (size: { width: number; height: number }) => void;
 
export function observeResize(callback: ResizeCallback, delay = 150) {
  let timeoutId: ReturnType<typeof setTimeout> | undefined;

Debouncing window resize in TypeScript

debounce closures event-listeners
Intermediate 6 steps
typescript
import { Injectable, Logger, OnApplicationShutdown } from '@nestjs/common';
import { InjectRedis } from '@nestjs-modules/ioredis';
import Redis from 'ioredis';
import { DataSource } from 'typeorm';

Graceful shutdown hooks in NestJS

graceful-shutdown lifecycle-hooks dependency-injection
Intermediate 7 steps

Share this explainer

Here's the card — post it anywhere.

Decoupling side effects with NestJS events — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code