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:
- Repo: https://github.com/railsstudent/nest-stripe-integration
- Stripe Token API: https://stripe.com/docs/api/tokens
- Stripe Customer API: https://stripe.com/docs/api/customers