Stripe Integration In NestJS

 2 total views,  2 views today

Scenario: Our ESG (Environmental, Social, and Governance) platform offers monthly and annual subscriptions to charge customers for using our ESG reporting service. When we designed our payment module, we chose Stripe as our payment platform because it accepts credit card payment method, provides good documentations and open source node library for the API (stripe-node).

Problem: We have chosen our payment platform but we don’t want the front-end application to call Stripe API directly. The architecture design is to make HTTP requests to NestJS backend and the backend calls node Stripe library to update Stripe accounts and our database. It is a challenge to our development team because we have never done Stripe integration in NestJS before and we intend to implement it by adopting good NestJS philosophy.

Go to Stripe.com to sign up a developer account

Copy the secret key as it will be passed to Stripe API to update Stripe accounts

Create a NestJS application

nest new nest-stripe-integration

Install Stripe and other dependencies

npm i stripe --save
npm i --save @nestjs/config joi

Store Stripe secret key in environment file

Store environment variables in .env and we will use configuration service to retrieve the secret key in the codes.

Import Configuration Module

Import configuration module and validate the schema of .env file.

Validation part completes and we can move on to create a Stripe module that is the core of this post.

Create a Stripe module to encapsulate node Stripe library

nest g mo Stripe

Next, I run nest commands to generate stripe service and stripe controller and the following is the declaration of the StripeModule:

@Module({
  providers: [StripeService],
  controllers: [StripeController],
})
export class StripeModule {}

Add logic to Stripe service to create and retrieve customer

The create customer logic is going to create a new credit card and assign it to the new customer. For brevity’s sake, I won’t show the implementation of createCard here and you can visit my github repo to find it.

// stripe.service.ts

import Stripe from 'stripe'

constructor(private service: ConfigService) {}

async createCustomer(dto: CreateCustomerDto): Promise<Stripe.Response<Stripe.Customer>> {
    const { card = null, email, name, description } = dto || {}

    if (!card) {
      throw new BadRequestException('Card information is not found')
    }
     
    const secretKey = this.service.get<string>('STRIPE_SECRET_KEY') || ''
    const stripe = new Stripe(secretKey, {
        apiVersion: '2020-08-27',
    })    

    const { data } = await stripe.customers.list({
      email,
      limit: 1,
    })

    if (data.length > 0) {
      throw new BadRequestException('Customer email is found')
    }

    const newCustomer = await stripe.customers.create({ email, name, description })
    if (!newCustomer) {
      throw new BadRequestException('Customer is not created')
    }

    await this.createCard(newCustomer.id, card)
    return newCustomer
}

The get customer logic accepts Stripe customer id and returns the Stripe customer object if it exists

async getCustomer(customerId: string): Promise<Stripe.Customer | null> {
    const secretKey = this.service.get<string>('STRIPE_SECRET_KEY') || ''
    const stripe = new Stripe(secretKey, {
        apiVersion: '2020-08-27',
    })

    const customer = await stripe.customers.retrieve(customerId)

    if (customer.deleted) {
      return null
    }

    const { headers, lastResponse, ...rest } = customer
    return rest as Stripe.Customer
  }

We have completed the Stripe service and can proceed to add endpoints to the controller.

Add new endpoints to the controller

@Controller('stripe')
export class StripeController {
  constructor(private service: StripeService) {}

  @Post('customer')
  async createCustomer(@Body() dto: CreateCustomerDto): Promise<Stripe.Response<Stripe.Customer>> {
    return this.service.createCustomer(dto)
  }

  @Get('customer/:customerId')
  async getCustomer(@Param('customerId') customerId: string): Promise<Stripe.Customer | null> {
    return this.service.getCustomer(customerId)
  }
}

The controller is also completed and we can get ourselves new customers.

CURL

// Create new customer
curl --location --request POST 'http://localhost:3000/stripe/customer' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "John Doe",
    "description": "Stripe Customer (john.doe@yopmail.com)",
    "email": "john.doe@yopmail.com",
    "card": {
        "name": "John Doe",
        "number": "4242424242424242",
        "expMonth": "01",
        "expYear": "2026",
        "cvc": "315" 
    }
}

// Response
{
    "id": "cus_Jx6caVHGwhTrAK",
    "object": "customer",
    "address": null,
    "balance": 0,
    "created": 1627715672,
    "currency": null,
    "default_source": null,
    "delinquent": false,
    "description": "Stripe Customer (john.doe@yopmail.com)",
    "discount": null,
    "email": "john.doe@yopmail.com",
    "invoice_prefix": "4D7BFE8B",
    "invoice_settings": {
        "custom_fields": null,
        "default_payment_method": null,
        "footer": null
    },
    "livemode": false,
    "metadata": {},
    "name": "John Doe",
    "next_invoice_sequence": 1,
    "phone": null,
    "preferred_locales": [],
    "shipping": null,
    "tax_exempt": "none"
}
// Retrieve customer
curl --location --request GET 'http://localhost:3000/stripe/customer/cus_Jx6caVHGwhTrAK'

// Response
{
    "id": "cus_Jx6caVHGwhTrAK",
    "object": "customer",
    "address": null,
    "balance": 0,
    "created": 1627715672,
    "currency": null,
    "default_source": "card_1JJCOzBX6xHuypyKQ5nw4PxY",
    "delinquent": false,
    "description": "Stripe Customer (john.doe@yopmail.com)",
    "discount": null,
    "email": "john.doe@yopmail.com",
    "invoice_prefix": "4D7BFE8B",
    "invoice_settings": {
        "custom_fields": null,
        "default_payment_method": null,
        "footer": null
    },
    "livemode": false,
    "metadata": {},
    "name": "John Doe",
    "next_invoice_sequence": 1,
    "phone": null,
    "preferred_locales": [],
    "shipping": null,
    "tax_exempt": "none"
}

At this point, Stripe module has successfully provided Stripe functionality but we can make improvements to Stripe service to make it dry

Improvement #1: Make a reusable function to return Node stripe client

If you observe closely, you will see that every function instantiates a node Stripe client to call the library in order to perform customer-related activity.

We can create a small function that returns the client and reuse it in other functions

initStripeClient() {
    const secretKey = this.service.get<string>('STRIPE_SECRET_KEY') || ''
    const stripe = new Stripe(secretKey, {
        apiVersion: '2020-08-27',
    })
    return stripe
}
// stripe.service.ts

import Stripe from 'stripe'

constructor(private service: ConfigService) {}

async createCustomer(dto: CreateCustomerDto): Promise<Stripe.Response<Stripe.Customer>> {
    const { card = null, email, name, description } = dto || {}

    if (!card) {
      throw new BadRequestException('Card information is not found')
    }
     
    const stripe = this.initStripeClient()

    const { data } = await stripe.customers.list({
      email,
      limit: 1,
    })

    if (data.length > 0) {
      throw new BadRequestException('Customer email is found')
    }

    const newCustomer = await stripe.customers.create({ email, name, description })
    if (!newCustomer) {
      throw new BadRequestException('Customer is not created')
    }

    await this.createCard(newCustomer.id, card)
    return newCustomer
}
async getCustomer(customerId: string): Promise<Stripe.Customer | null> {
    const stripe = this.initStripeClient()

    const customer = await stripe.customers.retrieve(customerId)

    if (customer.deleted) {
      return null
    }

    const { headers, lastResponse, ...rest } = customer
    return rest as Stripe.Customer
 }

We can take one step further by eliminating initStripeClient and injecting the Stripe client in the constructor. How do we do that? The answer is to create a custom provider for the Stripe client in StripeModule.

Improvement #2: Create a custom provider for the Stripe client

// stripe.module.ts

const StripeClientProvider = {
  provide: 'StripeClient',
  inject: [ConfigService],
  useFactory: (service: ConfigService) => {
    const secretKey = service.get<string>('STRIPE_SECRET_KEY', '')
    const stripe = new Stripe(secretKey, {
      apiVersion: '2020-08-27',
    })
    return stripe
  },
}

@Module({
  providers: [StripeService, StripeClientProvider],
  controllers: [StripeController],
})
export class StripeModule {}

Improvement #3: Inject the Stripe client to the constructor of Stripe service

// stripe.service.ts

constructor(@Inject('StripeClient') private stripeClient: Stripe) {}

async createCustomer(dto: CreateCustomerDto): Promise<Stripe.Response<Stripe.Customer>> {
    const { card = null, email, name, description } = dto || {}

    if (!card) {
      throw new BadRequestException('Card information is not found')
    }

    const { data } = await this.stripeClient.customers.list({
      email,
      limit: 1,
    })

    if (data.length > 0) {
      throw new BadRequestException('Customer email is found')
    }

    const newCustomer = await this.stripeClient.customers.create({ email, name, description })
    if (!newCustomer) {
      throw new BadRequestException('Customer is not created')
    }

    await this.createCard(newCustomer.id, card)
    return newCustomer
}

async getCustomer(customerId: string): Promise<Stripe.Customer | null> {
    const customer = await this.stripeClient.customers.retrieve(customerId)

    if (customer.deleted) {
      return null
    }

    const { headers, lastResponse, ...rest } = customer
    return rest as Stripe.Customer
}

This is the final version of stripe service: the functions are DRY and it is possible because of useFactory and injection token provided by NestJs.

Final thoughts

In this post, I have shown the usage of useFactory to create an instance of node-stripe client and make it available in NestJS service through injection. Rather than creating a new stripe client in every method, the service keeps a single reference and uses it to call Stripe APIs to create new Stripe customer. This is an illustration of DRY principle where the methods are lean and without duplicated logic.

I hope you find this blog post helpful and look to add NestJS into your backend technical stack.

Resources:

  1. Repo: https://github.com/railsstudent/nest-stripe-integration
  2. Stripe Token API: https://stripe.com/docs/api/tokens
  3. Stripe Customer API: https://stripe.com/docs/api/customers

Dependency injection by abstract class in NestJS

 2 total views,  2 views today

When developers start using NestJS, they create new module with controllers and services to design backend APIs. Developers register controllers and services in the module and inject service into controller to perform CRUD operations.

Provider Registration

The following is a code snippet of a user module; user controller is registered in controller array while user service is registered in provider array .

@Module({
   controller: [UserController],
   provider: [UserService]
})
export class UserModule {}

[UserService] is concrete class registration and is the short form of

provider: [
  {
     provide: UserService
     useClass: UserService
  }
]

When UserService parameter is seen in the constructor of UserController, an instance of UserService injected.

Abstract Class Registration

At work, the business requirements of our application are to build different type of reports. Each type of report is encapsulated in a module and the controller defines an endpoint that exports report in PDF format and send the attachment to email users.

The naive implementation injects a service and executes functions in controller function to fulfill the purpose. The export function writes contents into a buffer stream and sendEmail attaches it to mail body and sends to some email addresses.

// User Report Module 

@Module({
   controller: [UserReportController],
   provider: [UserReportService]
})
export class UserReportModule {}
// User Report Controller

@Controller('user-report')
export class UserReportController {
   constructor(private service: UserReportService) {}

   @POST()
   async shareReport(): Promise<void> {
       const buffer = await this.service.export()
       await this.service.sendEmail(buffer, 'user1@gmail.com', 'user2@gmail.com')   
   } 
}
// User Report Service

@Injectable()
export class UserReportService {
   async export(): Promise<Buffer> {
    return Buffer.from('Export in user report service')
  }

  sendEmail(buffer: Buffer, ...emails: string[]): void {
    console.log('buffer: ', buffer.toString('utf-8'))
    console.log('send user report to emails:', emails)
  }
}

When there is one type of report, the codes pass our code review process. When the application enhances to support three types of reports; shareReport, export and sendEmail are copied to three modules and DRY principle is violated.

  • It is time to refactor the codes
  • It is time to refactor the codes
  • It is time to refactor the codes

Step 1: Create an abstract report service class in core directory

// Abstract report service

export abstract class AbstractReportService {
  abstract export(): Promise<Buffer>

  sendEmail(buffer: Buffer, ...emails: string[]): void {
    console.log('buffer: ', buffer.toString('utf-8'))
    console.log('send report to emails:', emails)
  }

  async shareReport(...emails: string[]): Promise<void> {
    const buffer = await this.export()
    await this.sendEmail(buffer, ...emails)
  }
}

AbstractReportService class defines shareReport function that exports PDF file and sends the buffer stream to email addresses. sendEmail is reused by different types of reports but export must be implemented by subclasses.

Step 2: Dependency injection by abstract class

Rewrite UserReportModule to provide registration of AbstractReportService

// User Report Module

@Module({
   controller: [UserReportController],
   provider: [
     { 
        provide: AbstractReportService,
        useClass: UserReportService
     }
   ]
})
export class UserReportModule {}
// User Report Controller 

@Controller('user-report')
export class UserReportController {
   constructor(private service: AbstractReportService) {}

   @POST()
   async shareReport(): Promise<void> {
       await this.service.shareReport('user1@gmail.com', 'user2@gmail.com')   
   } 
}
// User Report Service

@Injectable()
export class UserReportService {
   async export(): Promise<Buffer> {
    return Buffer.from('Export in user report service')
  }
}

Step 3: Add new report type after refactoring

When new report type is introduced to the application, the new module registers AbstractReportService and implements the abstract function. Suppose I create PaymentReport module to export a payment report.

// Payment Report Module

@Module({
  controllers: [PaymentReportController],
  providers: [
    {
      provide: AbstractReportService,
      useClass: PaymentReportService,
    },
  ],
})
export class PaymentReportModule {}
// Payment Report Controller

@Controller('payment-report')
export class PaymentReportController {
  constructor(private service: AbstractReportService) {}

  @Post()
  async shareReport(): Promise<void> {
    await this.service.shareReport('john.doe@gmail.com', 'jane.doe@gmail.com')
  }
}
// Payment Report Service

@Injectable()
export class PaymentReportService extends AbstractReportService {
  async export(): Promise<Buffer> {
    return Buffer.from('Export in payment report service')
  }
}

Step 4: CURL POST endpoints

curl -X POST "http://localhost:3000/user-report"
// buffer:  Export in user report service
// send report to emails: [ 'black.doe@gmail.com', 'white.doe@gmail.com' ]

curl -X POST "http://localhost:3000/payment-report"
// buffer:  Export in payment report service
// send report to emails: [ 'john.doe@gmail.com', 'jane.doe@gmail.com' ]

Final thoughts

Dependency injection by abstract class returns an instance of concrete service registered in @Module decorator. This solution has many benefits:

  1. DRY principle because reusable functions are defined in abstract class and subclasses implement module-specific behavior
  2. Shorter controller class since shareReport function is reduced to one line
  3. Write less testing code because shareReport and sendEmail are tested once each. Before refactoring, sendEmail is tested three times.

This is the end of the blog post! I hope you enjoy reading it and NestJS development.

Resources:

  1. Repo: https://github.com/railsstudent/nest-interface-injection
  2. https://dev.to/ef/nestjs-dependency-injection-with-abstract-classes-4g65