typescript 74 lines · 7 steps

Verifying Stripe webhooks in NestJS

A NestJS controller that authenticates raw Stripe webhook payloads by signature before dispatching each event to billing logic.

Explained by highlit
1import {
2 Controller,
3 Post,
4 Headers,
5 Req,
6 BadRequestException,
7 Logger,
8} from '@nestjs/common';
9import { ConfigService } from '@nestjs/config';
10import Stripe from 'stripe';
11import type { RawBodyRequest } from '@nestjs/common';
12import type { Request } from 'express';
13import { BillingService } from './billing.service';
14 
15@Controller('webhooks/stripe')
16export class StripeWebhookController {
17 private readonly logger = new Logger(StripeWebhookController.name);
18 private readonly stripe: Stripe;
19 private readonly webhookSecret: string;
20 
21 constructor(
22 config: ConfigService,
23 private readonly billing: BillingService,
24 ) {
25 this.stripe = new Stripe(config.getOrThrow('STRIPE_SECRET_KEY'), {
26 apiVersion: '2024-06-20',
27 });
28 this.webhookSecret = config.getOrThrow('STRIPE_WEBHOOK_SECRET');
29 }
30 
31 @Post()
32 async handle(
33 @Req() req: RawBodyRequest<Request>,
34 @Headers('stripe-signature') signature: string,
35 ) {
36 if (!req.rawBody) {
37 throw new BadRequestException('Missing raw request body');
38 }
39 
40 let event: Stripe.Event;
41 try {
42 event = this.stripe.webhooks.constructEvent(
43 req.rawBody,
44 signature,
45 this.webhookSecret,
46 );
47 } catch (err) {
48 this.logger.warn(`Signature verification failed: ${err.message}`);
49 throw new BadRequestException('Invalid Stripe signature');
50 }
51 
52 switch (event.type) {
53 case 'checkout.session.completed':
54 await this.billing.activateSubscription(
55 event.data.object as Stripe.Checkout.Session,
56 );
57 break;
58 case 'invoice.payment_failed':
59 await this.billing.flagPastDue(
60 event.data.object as Stripe.Invoice,
61 );
62 break;
63 case 'customer.subscription.deleted':
64 await this.billing.cancelSubscription(
65 event.data.object as Stripe.Subscription,
66 );
67 break;
68 default:
69 this.logger.debug(`Unhandled event type: ${event.type}`);
70 }
71 
72 return { received: true };
73 }
74}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Webhook endpoints must verify a cryptographic signature over the raw body to prove requests genuinely come from the provider.
  2. 2Signature checks need the untouched raw payload, so the framework must preserve it instead of only exposing parsed JSON.
  3. 3Dispatching on event type keeps the endpoint thin while delegating the real work to a dedicated service.

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.

Verifying Stripe webhooks in NestJS — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code