DEV Community

Sachith Wickramasekara
Sachith Wickramasekara

Posted on

πŸš€ Stripe Integration with NestJS + TypeORM: Let's Make It Happen! πŸ’³πŸ’»

Hey there, dev community! πŸ‘‹ Are you building a cool app and want to handle payments like a pro? Look no further! Today, we’re diving into Stripe integration with NestJS (using TypeORM πŸ—„οΈ), and I’ll walk you through the steps to get everything up and running smoothly πŸ˜‰. Let's dive right in! πŸš€

Step 1: First Things First β€” Stripe Keys πŸ”
Before we do anything, you’ll need to grab your Stripe Public and Secret Keys from the Stripe Dashboard.

🎟️ These keys are important because:

Public Key: This is used on the frontend to securely send card details to Stripe.
Secret Key: This stays in the backend and allows us to perform actions like creating customers, payment intents, etc. (πŸ’° we don't want this to leak!)
How to connect them in NestJS? Well, let’s use .env to store these keys securely!

# In .env
STRIPE_PUBLIC_KEY=pk_test_123
STRIPE_API_KEY=sk_test_456
Enter fullscreen mode Exit fullscreen mode

Next, you can inject this into your NestJS project using @nestjs/config. We’ll use this in our StripeModule. Keep reading! πŸ˜‰

Step 2: Setting Up Stripe in NestJS βš™οΈ
Alright, let’s install what we need in our NestJS project!

npm install @nestjs/config stripe @types/stripe
Enter fullscreen mode Exit fullscreen mode

Now, create a Stripe module. Here’s the code to get that rolling:

@Module({
  imports: [TypeOrmModule.forFeature([UserDetails])],
  controllers: [StripeController],
  providers: [
    StripeService,
    {
      provide: 'STRIPE_API_KEY',
      useFactory: async (configService: ConfigService) =>
        configService.get('STRIPE_API_KEY'),
      inject: [ConfigService],
    },
  ],
  exports: [StripeService],
})
export class StripeModule {}

Enter fullscreen mode Exit fullscreen mode

We import the UserDetails entity because we’ll be linking payments to users later! πŸ™Œ

Step 3: The Flow of Payments πŸ’³
Stripe payments follow a simple but powerful flow. Here's a bird's eye view of what we'll be doing:

  1. Be a Stripe customer πŸ§‘β€πŸ’Ό
  2. Create a payment intent πŸ’‘
  3. Attach payment methods πŸ’³
  4. Confirm the payment intent πŸ”’

Let’s break these steps down further! πŸ“πŸ‘‡

Step 4: Creating the Stripe Service πŸ› οΈ
First, we’ll initialize Stripe in our service by creating a stripe instance:

import Stripe from 'stripe';

@Injectable()
export class StripeService {
  private readonly stripe: Stripe;

  constructor(
    @Inject('STRIPE_API_KEY') private readonly apiKey: string,
  ) {
    this.stripe = new Stripe(this.apiKey, {
      apiVersion: '2024-06-20',
    });
  }
}

Enter fullscreen mode Exit fullscreen mode

That’s it! πŸŽ‰ Now our service is ready to handle all Stripe-related operations!

Step 5: Be a Stripe Customer πŸ‘₯
Before you can make any payments, you need to create a customer in Stripe! Here's a neat way to do it:

const stripeCustomer = await this.stripe.customers.create({
  email: 'john.doe@example.com',
  name: 'John Doe',
  phone: '555-555-5555',
  address: {
    line1: '123 NestJS St.',
    city: 'Codingville',
    country: 'US',
  },
  metadata: {
    userId: '12345', // Store anything you like here!
  },
});

Enter fullscreen mode Exit fullscreen mode

You can save the stripeCustomer.id to your database (in the UserDetails table or elsewhere). Don’t worry, you can always edit the details later! ✏️

Step 6: Creating Payment Intents 🎯
Now that you're a Stripe customer, let’s talk about creating a payment intent. The payment intent will tell Stripe how much to charge, which currency to use, and which payment methods are acceptable (we’ll use 'card' in this case πŸ’³).

async createPaymentIntent(
  amount: number,
  currency: string,
): Promise<Stripe.PaymentIntent> {
  try {
    const paymentIntent = await this.stripe.paymentIntents.create({
      amount,
      currency,
      payment_method_types: ['card'],
    });
    this.logger.log('Payment Intent created successfully');
    return paymentIntent;
  } catch (error) {
    this.logger.error('Failed to create Payment Intent', error.stack);
    throw new Error('Unable to create Payment Intent');
  }
}

Enter fullscreen mode Exit fullscreen mode
@Post('create-payment-intent')
  async createPaymentIntent(
    @Body('amount') amount: number,
    @Body('currency') currency: string,
  ): Promise<{ paymentIntentId: string }> {
    try {
      const paymentIntent = await this.stripeService.createPaymentIntent(
        amount,
        currency,
      );
      this.logger.log('Payment Intent created successfully');
      return { paymentIntentId: paymentIntent.id };
    } catch (error) {
      this.logger.error('Failed to create Payment Intent', error.stack);
      throw new HttpException(
        'Failed to create Payment Intent',
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }
  }
Enter fullscreen mode Exit fullscreen mode

Don't forget! After creating the intent, Stripe gives you a paymentIntent.id β€” store this in local storage or your database for future use! πŸ“¦

Step 7: Adding Payment Methods πŸ’³
Once you have a payment intent, you'll need a payment method. This links the customer’s payment details (like a card) to their account.

async addingPaymentMethod(
    userId: string,
    paymentMethodId: string,
  ): Promise<Stripe.PaymentMethod> {
    try {
      const userDetails = await this.userDetailsRepository.findOne({
        where: { userID: userId },
      });

      const paymentMethod = await this.stripe.paymentMethods.attach(
        paymentMethodId,
        {
          customer: userDetails.stripeCustomerId,
        },
      );

       return paymentMethod;
    } catch (error) {
      this.logger.error('Failed to add payment method', error.stack);
      throw error;
    }
  }

Enter fullscreen mode Exit fullscreen mode
 @Post('add-payment-method')
  async addPaymentMethod(
    @AuthenticatedUser() user: any,
    @Body('paymentMethodId') paymentMethodId: string,
  ): Promise<Stripe.PaymentMethod> {
    try {
      const paymentMethod = await this.stripeService.addPaymentMethod(
        user.sub,
        paymentMethodId,
      );
      this.logger.log('Payment method added successfully');
      return paymentMethod;
    } catch (error) {
      this.logger.error('Failed to add payment method', error.stack);
      throw new HttpException(
        'Failed to add payment method',
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }
  }
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ Note: You’ll need the Stripe customer ID (from Step 5) to attach a payment method!

Step 8: Retrieving Payment Methods πŸ“
Want to see all payment methods a customer has? You can easily fetch them using the following code:

async retrievePaymentMethods(
    userId: string,
  ): Promise<Stripe.PaymentMethod[]> {
    try {
      const userDetails = await this.userDetailsRepository.findOne({
        where: { userID: userId },
      });
      const paymentMethods = await this.stripe.paymentMethods.list({
        customer: userDetails.stripeCustomerId,
        type: 'card',
      });
      return paymentMethods.data;
    } catch (error) {
      this.logger.error('Failed to fetch payment methods', error.stack);
      throw error;
    }
  }

Enter fullscreen mode Exit fullscreen mode
  @Get('get-payment-methods')
  async retrievePaymentMethods(
    @AuthenticatedUser() user: any,
  ): Promise<Stripe.PaymentMethod[]> {
    try {
      const paymentMethods = await this.stripeService.retrievePaymentMethods(
        user.sub,
      );
      this.logger.log('Payment methods fetched successfully');
      return paymentMethods;
    } catch (error) {
      this.logger.error('Failed to fetch payment methods', error.stack);
      throw new HttpException(
        'Failed to fetch payment methods',
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }
  }
Enter fullscreen mode Exit fullscreen mode

Stripe will return a payment method ID for every card added. πŸ› οΈ Use it to confirm payments!

Step 9: Confirming the Payment Intent βœ…
The final piece is to confirm the payment intent. To do this, you need both the paymentIntentId and the paymentMethodId.

async paymentConfirmation(
    paymentIntentId: string,
    paymentMethodId: string,
  ): Promise<Stripe.PaymentIntent> {
    try {
      const confirmedPaymentIntent = await this.stripe.paymentIntents.confirm(
        paymentIntentId,
        {
          payment_method: paymentMethodId,
        },
      );
      return confirmedPaymentIntent;
    } catch (error) {
      this.logger.error('Failed to confirm Payment Intent', error.stack);
      throw new Error('Unable to confirm Payment Intent');
    }
  }

Enter fullscreen mode Exit fullscreen mode
  @Post('confirm-payment-intent')
  async confirmPaymentIntent(
    @Body('paymentIntentId') paymentIntentId: string,
    @Body('paymentMethodId') paymentMethodId: string,
    @AuthenticatedUser() user: any,
  ): Promise<Stripe.PaymentIntent> {
    try {
      const paymentIntent = await this.stripeService.confirmPaymentIntent(
        paymentIntentId,
        paymentMethodId,
        user.sub,
      );
      this.logger.log('Payment Intent confirmed successfully');
      return paymentIntent;
    } catch (error) {
      this.logger.error('Failed to confirm Payment Intent', error.stack);
      throw new HttpException(
        'Failed to confirm Payment Intent',
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }
  }
Enter fullscreen mode Exit fullscreen mode

Once the payment is confirmed, you can remove the paymentIntentId from local storage and redirect the user to a success screen! πŸŽ‰

🎬 Wrapping It Up

And that’s a wrap! 🎁 Integrating Stripe with NestJS isn’t rocket science, but with this guide, you’ll have a smooth ride from start to finish. 🌟 You'll be able to handle payments like a boss, and your users will love the seamless experience!

Have fun coding, and happy transactions! πŸ’Έ

Top comments (0)