- 1. API with NestJS #1. Controllers, routing and the module structure
- 2. API with NestJS #2. Setting up a PostgreSQL database with TypeORM
- 3. API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies
- 4. API with NestJS #4. Error handling and data validation
- 5. API with NestJS #5. Serializing the response with interceptors
- 6. API with NestJS #6. Looking into dependency injection and modules
- 7. API with NestJS #7. Creating relationships with Postgres and TypeORM
- 8. API with NestJS #8. Writing unit tests
- 9. API with NestJS #9. Testing services and controllers with integration tests
- 10. API with NestJS #10. Uploading public files to Amazon S3
- 11. API with NestJS #11. Managing private files with Amazon S3
- 12. API with NestJS #12. Introduction to Elasticsearch
- 13. API with NestJS #13. Implementing refresh tokens using JWT
- 14. API with NestJS #14. Improving performance of our Postgres database with indexes
- 15. API with NestJS #15. Defining transactions with PostgreSQL and TypeORM
- 16. API with NestJS #16. Using the array data type with PostgreSQL and TypeORM
- 17. API with NestJS #17. Offset and keyset pagination with PostgreSQL and TypeORM
- 18. API with NestJS #18. Exploring the idea of microservices
- 19. API with NestJS #19. Using RabbitMQ to communicate with microservices
- 20. API with NestJS #20. Communicating with microservices using the gRPC framework
- 21. API with NestJS #21. An introduction to CQRS
- 22. API with NestJS #22. Storing JSON with PostgreSQL and TypeORM
- 23. API with NestJS #23. Implementing in-memory cache to increase the performance
- 24. API with NestJS #24. Cache with Redis. Running the app in a Node.js cluster
- 25. API with NestJS #25. Sending scheduled emails with cron and Nodemailer
- 26. API with NestJS #26. Real-time chat with WebSockets
- 27. API with NestJS #27. Introduction to GraphQL. Queries, mutations, and authentication
- 28. API with NestJS #28. Dealing in the N + 1 problem in GraphQL
- 29. API with NestJS #29. Real-time updates with GraphQL subscriptions
- 30. API with NestJS #30. Scalar types in GraphQL
- 31. API with NestJS #31. Two-factor authentication
- 32. API with NestJS #32. Introduction to Prisma with PostgreSQL
- 33. API with NestJS #33. Managing PostgreSQL relationships with Prisma
- 34. API with NestJS #34. Handling CPU-intensive tasks with queues
- 35. API with NestJS #35. Using server-side sessions instead of JSON Web Tokens
- 36. API with NestJS #36. Introduction to Stripe with React
- 37. API with NestJS #37. Using Stripe to save credit cards for future use
- 38. API with NestJS #38. Setting up recurring payments via subscriptions with Stripe
- 39. API with NestJS #39. Reacting to Stripe events with webhooks
- 40. API with NestJS #40. Confirming the email address
- 41. API with NestJS #41. Verifying phone numbers and sending SMS messages with Twilio
- 42. API with NestJS #42. Authenticating users with Google
- 43. API with NestJS #43. Introduction to MongoDB
- 44. API with NestJS #44. Implementing relationships with MongoDB
- 45. API with NestJS #45. Virtual properties with MongoDB and Mongoose
- 46. API with NestJS #46. Managing transactions with MongoDB and Mongoose
- 47. API with NestJS #47. Implementing pagination with MongoDB and Mongoose
- 48. API with NestJS #48. Definining indexes with MongoDB and Mongoose
- 49. API with NestJS #49. Updating with PUT and PATCH with MongoDB and Mongoose
- 50. API with NestJS #50. Introduction to logging with the built-in logger and TypeORM
- 51. API with NestJS #51. Health checks with Terminus and Datadog
- 52. API with NestJS #52. Generating documentation with Compodoc and JSDoc
- 53. API with NestJS #53. Implementing soft deletes with PostgreSQL and TypeORM
- 54. API with NestJS #54. Storing files inside a PostgreSQL database
- 55. API with NestJS #55. Uploading files to the server
- 56. API with NestJS #56. Authorization with roles and claims
- 57. API with NestJS #57. Composing classes with the mixin pattern
- 58. API with NestJS #58. Using ETag to implement cache and save bandwidth
- 59. API with NestJS #59. Introduction to a monorepo with Lerna and Yarn workspaces
- 60. API with NestJS #60. The OpenAPI specification and Swagger
- 61. API with NestJS #61. Dealing with circular dependencies
- 62. API with NestJS #62. Introduction to MikroORM with PostgreSQL
- 63. API with NestJS #63. Relationships with PostgreSQL and MikroORM
- 64. API with NestJS #64. Transactions with PostgreSQL and MikroORM
- 65. API with NestJS #65. Implementing soft deletes using MikroORM and filters
- 66. API with NestJS #66. Improving PostgreSQL performance with indexes using MikroORM
- 67. API with NestJS #67. Migrating to TypeORM 0.3
- 68. API with NestJS #68. Interacting with the application through REPL
- 69. API with NestJS #69. Database migrations with TypeORM
- 70. API with NestJS #70. Defining dynamic modules
- 71. API with NestJS #71. Introduction to feature flags
- 72. API with NestJS #72. Working with PostgreSQL using raw SQL queries
- 73. API with NestJS #73. One-to-one relationships with raw SQL queries
- 74. API with NestJS #74. Designing many-to-one relationships using raw SQL queries
- 75. API with NestJS #75. Many-to-many relationships using raw SQL queries
- 76. API with NestJS #76. Working with transactions using raw SQL queries
- 77. API with NestJS #77. Offset and keyset pagination with raw SQL queries
- 78. API with NestJS #78. Generating statistics using aggregate functions in raw SQL
- 79. API with NestJS #79. Implementing searching with pattern matching and raw SQL
- 80. API with NestJS #80. Updating entities with PUT and PATCH using raw SQL queries
- 81. API with NestJS #81. Soft deletes with raw SQL queries
- 82. API with NestJS #82. Introduction to indexes with raw SQL queries
- 83. API with NestJS #83. Text search with tsvector and raw SQL
- 84. API with NestJS #84. Implementing filtering using subqueries with raw SQL
- 85. API with NestJS #85. Defining constraints with raw SQL
- 86. API with NestJS #86. Logging with the built-in logger when using raw SQL
- 87. API with NestJS #87. Writing unit tests in a project with raw SQL
- 88. API with NestJS #88. Testing a project with raw SQL using integration tests
- 89. API with NestJS #89. Replacing Express with Fastify
- 90. API with NestJS #90. Using various types of SQL joins
- 91. API with NestJS #91. Dockerizing a NestJS API with Docker Compose
- 92. API with NestJS #92. Increasing the developer experience with Docker Compose
- 93. API with NestJS #93. Deploying a NestJS app with Amazon ECS and RDS
- 94. API with NestJS #94. Deploying multiple instances on AWS with a load balancer
- 95. API with NestJS #95. CI/CD with Amazon ECS and GitHub Actions
- 96. API with NestJS #96. Running unit tests with CI/CD and GitHub Actions
- 97. API with NestJS #97. Introduction to managing logs with Amazon CloudWatch
- 98. API with NestJS #98. Health checks with Terminus and Amazon ECS
- 99. API with NestJS #99. Scaling the number of application instances with Amazon ECS
- 100. API with NestJS #100. The HTTPS protocol with Route 53 and AWS Certificate Manager
- 101. API with NestJS #101. Managing sensitive data using the AWS Secrets Manager
- 102. API with NestJS #102. Writing unit tests with Prisma
- 103. API with NestJS #103. Integration tests with Prisma
- 104. API with NestJS #104. Writing transactions with Prisma
- 105. API with NestJS #105. Implementing soft deletes with Prisma and middleware
- 106. API with NestJS #106. Improving performance through indexes with Prisma
- 107. API with NestJS #107. Offset and keyset pagination with Prisma
- 108. API with NestJS #108. Date and time with Prisma and PostgreSQL
- 109. API with NestJS #109. Arrays with PostgreSQL and Prisma
- 110. API with NestJS #110. Managing JSON data with PostgreSQL and Prisma
- 111. API with NestJS #111. Constraints with PostgreSQL and Prisma
- 112. API with NestJS #112. Serializing the response with Prisma
- 113. API with NestJS #113. Logging with Prisma
- 114. API with NestJS #114. Modifying data using PUT and PATCH methods with Prisma
- 115. API with NestJS #115. Database migrations with Prisma
- 116. API with NestJS #116. REST API versioning
- 117. API with NestJS #117. CORS – Cross-Origin Resource Sharing
- 118. API with NestJS #118. Uploading and streaming videos
- 119. API with NestJS #119. Type-safe SQL queries with Kysely and PostgreSQL
- 120. API with NestJS #120. One-to-one relationships with the Kysely query builder
- 121. API with NestJS #121. Many-to-one relationships with PostgreSQL and Kysely
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:
1 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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
1 2 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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
1 2 3 4 5 6 7 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
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<User>, ) {} async updateMonthlySubscriptionStatus( stripeCustomerId: string, monthlySubscriptionStatus: string ) { return this.usersRepository.update( { stripeCustomerId }, { monthlySubscriptionStatus } ); } // ... } |
To use the above logic, we need to expand our StripeWebhookController:
stripeWebhook.controller.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import { Controller, Post, Headers, Req, BadRequestException } from '@nestjs/common'; import StripeService from '../stripe/stripe.service'; import RequestWithRawBody from './requestWithRawBody.interface'; import { UsersService } from '../users/users.service'; import Stripe from 'stripe'; @Controller('webhook') export default class StripeWebhookController { constructor( private readonly stripeService: StripeService, private readonly usersService: UsersService ) {} @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); if (event.type === 'customer.subscription.updated' || event.type === 'customer.subscription.created') { const data = event.data.object as Stripe.Subscription; const customerId: string = data.customer as string; const subscriptionStatus = data.status; await this.usersService.updateMonthlySubscriptionStatus(customerId, subscriptionStatus) } } } |
Above, we had to sort out some TypeScript issues. Currently, Stripe recommends casting to deal with them.
In our flow, Stripe calls our /webhook endpoint and sends us events. We check if they are connected to subscriptions by checking the event.type property. If that’s the case, we can assume that the event.data.object property is a subscription. With that knowledge, we can update the monthlySubscriptionStatus property of a user.
Webhook idempotency
According to the Stripe documentation, Stripe might occasionally send the same event more than once. They advise us to create a mechanism to guard the application against processing the same event multiple times and making our event processing idempotent.
One way of doing so would be to keep the id of every processed event in the database.
stripeEvent.entity.ts
1 2 3 4 5 6 7 8 9 |
import { Entity, PrimaryColumn } from 'typeorm'; @Entity() class StripeEvent { @PrimaryColumn() public id: string; } export default StripeEvent; |
Please notice that above we define a primary column that is not auto-generated. We aim to use the event id from Stripe to populate this column.
stripeWebhook.service.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import StripeEvent from './StripeEvent.entity'; import { Repository } from 'typeorm'; @Injectable() export default class StripeWebhookService { constructor( @InjectRepository(StripeEvent) private eventsRepository: Repository<StripeEvent> ) {} createEvent(id: string) { return this.eventsRepository.insert({ id }); } } |
A crucial thing to notice is that the createEvent throws an error when we try to use an id that already exists in the database. We can use it to improve our StripeWebhookController.
stripeWebhook.controller.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import { Controller, Post, Headers, Req, BadRequestException } from '@nestjs/common'; import StripeService from '../stripe/stripe.service'; import RequestWithRawBody from './requestWithRawBody.interface'; import { UsersService } from '../users/users.service'; import StripeWebhookService from './stripeWebhook.service'; @Controller('webhook') export default class StripeWebhookController { constructor( private readonly stripeService: StripeService, private readonly usersService: UsersService, private readonly stripeWebhookService: StripeWebhookService ) {} @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); if (event.type === 'customer.subscription.updated' || event.type === 'customer.subscription.created') { return this.stripeWebhookService.processSubscriptionUpdate(event); } } } |
Since our controller keeps growing, let’s move part of the logic to our StripeWebhookService.
stripeWebhook.service.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
import { BadRequestException, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import StripeEvent from './StripeEvent.entity'; import { Repository } from 'typeorm'; import Stripe from 'stripe'; import PostgresErrorCode from '../database/postgresErrorCode.enum'; import { UsersService } from '../users/users.service'; @Injectable() export default class StripeWebhookService { constructor( @InjectRepository(StripeEvent) private eventsRepository: Repository<StripeEvent>, private readonly usersService: UsersService, ) {} createEvent(id: string) { return this.eventsRepository.insert({ id }); } async processSubscriptionUpdate(event: Stripe.Event) { try { await this.createEvent(event.id); } catch (error) { if (error?.code === PostgresErrorCode.UniqueViolation) { throw new BadRequestException('This event was already processed'); } } const data = event.data.object as Stripe.Subscription; const customerId: string = data.customer as string; const subscriptionStatus = data.status; await this.usersService.updateMonthlySubscriptionStatus(customerId, subscriptionStatus); } } |
With the above code, our endpoint throws an error when Stripe sends the same event again.
Deleting old events with cron might be a good idea. If you want to do that, check out API with NestJS #25. Sending scheduled emails with cron and Nodemailer
Summary
In this article, we’ve learned more about Stripe and improved our application by reacting to Stripe events. To do that, we’ve had to implement a webhook that accepts requests from Stripe. When doing so, we’ve started to track changes in the subscription statuses. We’ve also made sure that we don’t parse the same event more than once.
There is so much useful information. Not only in this article but also from others. Thank you!
You don’t need a middleware for raw bodies anymore. You can just do this:
Cant believe I found the solution here xD thanks for it!
good article but I think to ensure idempotency we need to run both inserting StripeEvent and changing User’s state as part of db transaction.
Another thing – we shouldn’t throw error when event was already processed because Stripe will retry.