Intro
Imagine that you have an application with a 3rd-party service integration (e.g. Stripe, Sendgrid, etc.). Of course, the application is deployed to different environments (dev, staging, prod).
Let’s say, we have to listen to some webhook events from a 3rd party integration (for instance, Sendgrid). Here comes the problem: what if the integration supports only one webhook endpoint to post events to (Sendgrid does)? Which environment should we post events to? Is is staging, prod or test?
Of course, we should be able to listen to webhook events on any environment. What’s the solution?
It’s easy. Webhook events will be published to the most stable environment only (e.g. prod) and then will be reposted to other environments.
Solution
Firstly, create a new module:
nest g module webhooks
.
Edit webhooks.controller.ts
:
@Controller('webhooks')
export class WebhooksController {
constructor(private readonly service: WebhooksService) {}
@Post('/example')
@UseInterceptors(RepostWebhookInterceptor('example'))
handler(@Body() body: any) {
//
}
}
This API will be available by /webhooks/example
in all environments.
Interceptor
import { HttpService } from '@nestjs/axios';
import { CallHandler, ExecutionContext, Inject, mixin, NestInterceptor } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { from, lastValueFrom, Observable, tap } from 'rxjs';
export function RepostWebhookInterceptor(hookName: string): any {
class RepostWebhookMixin implements NestInterceptor {
constructor(
private readonly httpService: HttpService,
@Inject(ConfigService) private readonly configService: ConfigService,
) {}
intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Observable<any> | Promise<Observable<any>> {
const body = context.switchToHttp().getRequest().body;
const isEnabled = this.configService.get('SHOULD_REPOST_WEBHOOKS', { infer: true });
const urls = JSON.parse(this.configService.get('WEBHOOK_REPOST_URLS', { infer: true }));
if (isEnabled) {
return next
.handle()
.pipe(
tap(() =>
from(urls).forEach((url) =>
lastValueFrom(this.httpService.post(url + hookName, body)),
// you can add authorization here, if needed
),
),
);
}
return next.handle();
}
}
return mixin(RepostWebhookMixin);
}
As you can see we have two env
variables: SHOULD_REPOST_WEBHOOKS
and WEBHOOK_REPOST_URLS
.
The first variable must be set to true
only on the one of the environments.
Example
Production .env
:
SHOULD_REPOST_WEBHOOKS=true
WEBHOOK_REPOST_URLS=["https://test.env/webhooks/", "https://staging.env/webhooks/"]
The second variable should include the webhook controller path. In our example, it’s /webhooks/
.
So, RepostWebhookInterceptor
receives a hook name that should be reposted.
@Post('/example')
@UseInterceptors(RepostWebhookInterceptor('example'))
The hook name must be the same as the webhook handler URL.
Now, when webhook handler is triggered it’ll also check if it should repost this event to any other environment.
Okay, as you see, this method just transmits webhooks to other urls. It’s the easiest solution and it works.