Reacting to Stripe events with webhooks
So far, in this series, we’ve interacted with Stripe by sending requests. It was either by requesting the Stripe API directly on the frontend, or the backend.…
So far, in this series, we’ve interacted with Stripe by sending requests. It was either by requesting the Stripe API directly on the frontend, or the backend. With webhooks, Stripe can communicate with us the other way around. Webhook is a URL in our API that Stripe can request to send us various events such as information about payments or customer updates. In this article, we explore the idea of Webhooks and implement them into our application to avoid asking Stripe about the status of user’s subscriptions. By doing that, we aim to improve the performance of our application and avoid exceeding rate limits. Using Stripe webhooks with NestJS We aim to develop with Stripe webhooks while running the application on localhost. When working with webhooks, we expect Stripe to make requests to our API. By default, our app can’t be accessed from outside while running locally. Because of that, we need an additional step to test webhooks. To perform it, we need Stripe CLI. We can download it here. We need to forward received events to our local API. To do it, we need to run the following:stripe listen --forward-to localhost:3000/webhook Handling webhook signing secret In response, we receive the webhook signing secret. We will need it in our API to validate requests made to our /webhook endpoint. A valid approach is to keep the webhook secret in our environment variables. app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from '@hapi/joi'; @Module({ imports: [ ConfigModule.forRoot({ validationSchema: Joi.object({ STRIPE_WEBHOOK_SECRET: Joi.string(), // ... }) }), // ... ], controllers: [], providers: [], }) export class AppModule {} .env STRIPE_WEBHOOK_SECRET=whsec_... # ... Accessing the raw body of a request NestJS uses the body-parser library to parse incoming request bodies. Because of that, we don’t get to access the raw body straightforwardly. The Stripe package that we need to use to work with webhooks requires it, though. To deal with the above issue, we can create a middleware that attaches the raw body to the request. rawBody.middleware.ts import { Response } from 'express'; import { json } from 'body-parser'; import RequestWithRawBody from '../stripeWebhook/requestWithRawBody.interface'; function rawBodyMiddleware() { return json({ verify: (request: RequestWithRawBody, response: Response, buffer: Buffer) => { if (request.url === '/webhook' && Buffer.isBuffer(buffer)) { request.rawBody = Buffer.from(buffer); } return true; }, }) } export default rawBodyMiddleware If you want to know more about middleware, check out TypeScript Express tutorial #1. Middleware, routing, and controllers Above, we use the RequestWithRawBody interface. We need to define it. requestWithRawBody.interface.ts import { Request } from 'express'; interface RequestWithRawBody extends Request { rawBody: Buffer; } export default RequestWithRawBody;For the middleware to work, we need to use it in our bootstrap function. main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import rawBodyMiddleware from './utils/rawBody.middleware'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.use(rawBodyMiddleware()); // ... await app.listen(3000); } bootstrap(); Parsing the webhook request When Stripe requests our webhook route, we need to parse the request. To do that successfully, we need three things: the webhook secret, the raw request payload, the stripe-signature request header. With the stripe-signature header we can verify that the events were sent by Stripe and not by some third party. When we have all of the above, we can use the Stripe library to construct the event data. stripe.service.ts import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import Stripe from 'stripe'; @Injectable() export default class StripeService { private stripe: Stripe; constructor( private configService: ConfigService ) { this.stripe = new Stripe(configService.get('STRIPE_SECRET_KEY'), { apiVersion: '2020-08-27', }); } public async constructEventFromPayload(signature: string, payload: Buffer) { const webhookSecret = this.configService.get('STRIPE_WEBHOOK_SECRET'); return this.stripe.webhooks.constructEvent( payload, signature, webhookSecret ); } // ... }The last step in managing the Stripe webhook with NestJS is to create a controller with the /webhook route. stripeWebhook.controller.ts import { Controller, Post, Headers, Req, BadRequestException } from '@nestjs/common'; import StripeService from '../stripe/stripe.service'; import RequestWithRawBody from './requestWithRawBody.interface'; @Controller('webhook') export default class StripeWebhookController { constructor( private readonly stripeService: StripeService, ) {} @Post() async handleIncomingEvents( @Headers('stripe-signature') signature: string, @Req() request: RequestWithRawBody ) { if (!signature) { throw new BadRequestException('Missing stripe-signature header'); } const event = await this.stripeService.constructEventFromPayload(signature, request.rawBody); // ... } } Tracking the status of subscriptions One of the things we could do with webhooks is tracking the status of subscriptions. To do that, let’s expand the User entity. user.entity.ts import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity() class User { @PrimaryGeneratedColumn() public id: number; @Column({ unique: true }) public email: string; @Column({ nullable: true }) public monthlySubscriptionStatus?: string; // ... } export default User;We also need a way to set the monthlySubscriptionStatus property. To do that, we need a new method in our UsersService: users.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, Connection, In } from 'typeorm'; import User from './user.entity'; import { FilesService } from '../files/files.service'; import StripeService from '../stripe/stripe.service'; @Injectable() export class UsersService { constructor( @InjectRepository(User) private usersRepository: Repository