How to Create an Intercepting System in Angular (e.g. WebSockets)

Alexander Lodygin
ITNEXT
Published in
4 min readJun 26, 2023

--

Photo by Dmitri Zotov on Unsplash

Angular is a powerful framework that contains a lot of ready-to-use solutions. One of them is the HttpClientModule. This module, besides creating HTTP requests, allows developers to use interceptors. This is an important Angular feature which helps handle requests and responses.

Sometimes developers have to manage data in another way (it could be via WebSockets, LocalStorage, PostMessage, or even EventBus within the app), and the need for some kind of interceptor may naturally arise.

So here, we’re reaching the point where a lot of developers start to create their own solutions but a majority of those solutions contradict the Angular concept. I want to demonstrate the main idea of creating an intercepting system in any data-management service. I’ll use a simple WebSocket service as an example:

export enum WsEvent {
ITEMS = 'ITEMS',
NOTIFICATIONS = 'NOTIFICATIONS',
}

export interface WsMessage<T = unknown> {
event: WsEvent;
data?: T;
}

@Injectable({
providedIn: 'root',
})
export class WsService {
private ws: WebSocketSubject<WsMessage> | null = null;
private readonly message$ = new Subject<WsMessage>();

public on<T>(event: WsEvent): Observable<WsMessage<T>> {
const message$ = this.message$.asObservable() as Observable<WsMessage<T>>;

return message$.pipe(filter((message) => message.event === event));
}

public send<T = unknown>(message: WsMessage<T>): void {
this.ws?.next(message);
}

public connect(): void {
this.ws = new WebSocketSubject({
url: WS_URL,
});

this.ws.subscribe((message) => this.message$.next(message));
}
}

Test component:

export interface Counter {
counter: number;
}

export interface Notifications {
amount?: number;
}

@Component({
standalone: true,
template: `
<button (click)="onSend()" type="button">Send message</button>
`,
})
export class App implements OnInit {
private readonly wsService = inject(WsService);
private counter = 0;

public ngOnInit(): void {
this.wsService.connect();

this.wsService
.on(WsEvent.NOTIFICATIONS)
.subscribe((message) => console.log(message));
}

public onSend(): void {
const data: Counter = { counter: this.counter++ };

this.wsService.send({
event: WsEvent.ITEMS,
data,
});
}
}

I’ve clicked 3 times on the button and here are my requests:

Sent messages after 3 clicks on the button

My goal is to double the counter value on each request and handle only event ITEMS.

First of all, I need to create an interface for the interceptor services and an InjectionToken for getting those.

export interface WsInterceptor<T = unknown> {
intercept(message: WsMessage<T>): WsMessage<T>;
}

export const WS_INTERCEPTORS = new InjectionToken<WsInterceptor[]>('');

It gives me an opportunity to prepare the first interceptor and provide it.

@Injectable()
export class DoubleCounterInterceptor implements WsInterceptor<Counter> {
public intercept(message: WsMessage<Counter>): WsMessage<Counter> {
if (message.event === WsEvent.ITEMS) {
message.data!.counter *= 2;
}

return message;
}
}
bootstrapApplication(App, {
providers: [{
provide: WS_INTERCEPTORS,
useClass: DoubleCounterInterceptor,
multi: true,
}]
});

Next step — create a handler service where I’ll be able to mutate messages. There is a method handle where I get a list of the interceptors by the recently created token, and execute the intercept method of all provided classes in a chain.

@Injectable({
providedIn: 'root',
})
export class WsHandlerService {
private readonly injector = inject(Injector);

public handle<D>(message: WsMessage<D>): WsMessage<D> {
const interceptors = this.injector.get<WsInterceptor<D>[]>(
WS_INTERCEPTORS,
[]
);

return interceptors.reduce(
(handledMessage, interceptor) => interceptor.intercept(handledMessage),
message
);
}
}

The last step is to modify the WsService and call the handle method from the WsHandlerService.

@Injectable({
providedIn: 'root',
})
export class WsService {
private ws: WebSocketSubject<WsMessage> | null = null;
private readonly wsHandlerService = inject(WsHandlerService);
private readonly message$ = new Subject<WsMessage>();

public on<T>(event: WsEvent): Observable<WsMessage<T>> {
const message$ = this.message$.asObservable() as Observable<WsMessage<T>>;

return message$.pipe(
filter((message) => message.event === event),
map((message) => this.wsHandlerService.handle(message))
);
}

public send<T = unknown>(message: WsMessage<T>): void {
const handledMessage = this.wsHandlerService.handle(message);
this.ws?.next(handledMessage);
}

public connect(): void {
this.ws = new WebSocketSubject({
url: WS_URL,
});

this.ws.subscribe((message) => this.message$.next(message));
}
}

Now, everything is ready. Let’s check the result.

Sent messages after 3 clicks on the button (handled by the interceptor)

Ok, but what about received messages?

I’ll create a new interceptor for handling theNOTIFICATIONS event and provide it. I want to normalize responses from the server and prevent additional handling from the component or other services.

@Injectable()
export class NormalizeNotificationsInterceptor
implements WsInterceptor<Notifications>
{
public intercept(
message: WsMessage<Notifications>
): WsMessage<Notifications> {
if (message.event !== WsEvent.NOTIFICATIONS) {
return message;
}

if (!message.data?.amount) {
message.data = { amount: 0 };
}

return message;
}
}
bootstrapApplication(App, {
providers: [
{
provide: WS_INTERCEPTORS,
useClass: DoubleCounterInterceptor,
multi: true,
},
{
provide: WS_INTERCEPTORS,
useClass: NormalizeNotificationsInterceptor,
multi: true,
},
],
});

Then, I’ll send from the server one payload without a data field and another with a value.

Received messages in the application (network tab)
Received messages in the test component

So, by following the steps outlined in the article and utilizing the provided example, you can create your own intercepting systems to enhance the functionality of your Angular applications.

Obviously, you should go further and modify the example, using more streams and different factories that allow you to identify the type of the events (e.g. SEND, GET/RECEIVE, ERROR).

One key point here is you must try to follow Angular approaches and use all available tools. Otherwise, you can lose logic distribution balance and overwhelm some part of your application in terms of support. Interceptors are powerful tools that will help you, even if you create your own handling system of them.

--

--