Stripe Integration In NestJS

Reading Time: 6 minutes

 166 total views,  4 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

How I cache data list in memory using useFactory in NestJS

Reading Time: 3 minutes

 170 total views,  2 views today

Scenario: Application at work stores data lists in database tables and they never change after initial population. These data lists are used for dropdown selection in frontend, for example, users select their salutation in user profile page and click to make the change.

Problem: When user opens the page, UI makes a HTTP request to retrieve the salutation list from the server. NestJS API connects the database to query the table and returns the salutation array in the response. Since the list is static, I want to cache the data in memory such that subsequent database calls can be avoided.

Solution #1: When application starts, store data lists in Redis and the API fetches the data from the in-memory cache.

Cons: This solution will require DevOps engineers to provision resources for the Redis instances in the cloud platform and I cannot justify the cost for setting them up for few data lists.

Solution #2: Use useFactory to create a custom provider to store the data lists in memory and use @Inject() to inject the cache in controller to return the cached data

Pros:

  1. The implementation is done within application level and no DevOps help is required.
  2. If data list changes in the future, TypeORM migration script will be deployed to the environment and application restarts to recreate the cache.

Lets start with the naive approach that queries database upon every http request

Naive Approach

Step 1: Create a service that waits 2 seconds before returning a list of salutations

async getSalutations(): Promise<Salutation> {
    const salutations = ['Mr.', 'Mrs.', 'Miss']
    return new Promise((resolve) => setTimeout(resolve, 2000)).then(() => {
      console.log('Wait 2 seconds before returning salutations array')
      const now = Date.now()
      return {
        now,
        salutations,
      }
    })
  }

The promise returns Data.now() to indicate that the result is obtained at real time.

Step 2: Create an endpoint to return the array of salutations

  @Get('salutations-realtime')
  async getRealtimeSalutations(): Promise<Salutation> {
    return this.appService.getSalutations()
  }

Step 3: Curl http://localhost:3000/salutations-realtime to show the timestamp changes every time

Take note that the timestamp is different each time because each call waits 2 seconds before the array is returned in the response.

Next, I will use useFactory to define a custom provider such that getSalutations() is called only once and the 2-second wait is a fixed cost.

Return data as fast as a cheetah

Custom Provider Approach

Step 1: Define interface of the map of data lists

export interface Salutation {
  now: number
  salutations: string[]
}

export interface DataMap {
  salutations: Salutation
}

Step 2: Add a new function in AppService to return the map of data lists

async getDataMap(): Promise<DataMap> {
   return {
      salutations: await this.getSalutations(),
   }
}

I used await this.getSalutations() to wait for the result and to store it in salutations property of the map.

Step 3: Create a custom provider for the map and register it in AppModule

const DataMapFactory = {
  provide: 'DataMap',
  useFactory: (appService: AppService) => {
    return appService.getDataMap()
  },
  inject: [AppService],
}

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService, DataMapFactory],
  exports: [DataMapFactory],
})
export class AppModule {}

Whenever I inject the provider name ‘DataMap‘, I have access to the map and use ES6 de-structuring to obtain salutations array

Step 4: Inject ‘DataMap’ in AppController and add a new /salutations endpoint to verify that it actually does what I intend

  constructor(private readonly appService: AppService, @Inject('DataMap') private dataMap: DataMap) {}

  @Get('salutations')
  async getSalutations(): Promise<Salutation> {
    const { salutations } = this.dataMap
    return salutations
  }

Last step: CURL /salutations and verify the timestamp does not change

All CURL responses have the same timestamp and Date.now() can only increase; therefore, /salutations endpoint returns the array from the cached map.

Final thoughts

In this post, I have shown one use case of useFactory of NestJS but useFactory can cache any resource, for example, database connection and instances of third-party libraries. In the future, I can write an article on how to create a custom provider for Node Stripe library in NestJS.

Resources:

  1. Repo: https://github.com/railsstudent/nest-poc-standard-version
  2. Custom Provider: https://docs.nestjs.com/fundamentals/custom-providers