If you’ve been around the internet long enough, you’ve probably encountered Stripe in one way or another. It’s used by countless startups, indie creators, and massive companies to handle payments with minimal fuss. Today, it’s arguably one of the most popular choices for developers who want to accept payments online without dealing with the mountain of complexities that come with payment systems.

Meanwhile, NestJS has quickly gained momentum for being a Node.js framework that sports strong architectural patterns and TypeScript support—two attributes that many modern developers find attractive. Put them together, and you get a powerful combo for building dependable, structured, and maintainable payment-related features.

This article takes you through the basics of integrating Stripe with a NestJS application. You’ll learn how to set up your NestJS project and configure a Stripe Module that manages your payment flows in a clean and reusable way.

You’ll see how to handle common tasks like creating payment intents, listing products, and issuing refunds. Although we’ll go step by step, a basic familiarity with NestJS and TypeScript will be helpful—but don’t worry if you’re just starting out. We’ll keep things very simple.

Let's jump right in, and by the end, you should be able to create and manage payments or subscriptions in your own NestJS application using the official Stripe SDK.

What You’ll Learn

  1. Project Setup: How to create a new NestJS project and install the necessary dependencies for Stripe.
  2. Configuring Stripe: Methods for storing and injecting the Stripe API key using NestJS’s powerful dependency injection system.
  3. Core Stripe Operations: How to handle essential flows in Stripe, including:
    • Listing products
    • Creating customers
    • Creating payment intents
    • Subscriptions
    • Issuing refunds
    • Generating payment links
  4. Organizing Code: Strategies for placing Stripe logic in dedicated service and controller files for better maintainability.
  5. Next Steps: Ways to expand your integration, from leveraging webhooks to handle advanced business logic to better securing your integration.

If that sounds interesting to you, let’s get building!

What You’ll Need

Before you write any code, ensure you have the following:

  1. Node.js and npm: You should have Node.js installed (preferably the latest LTS version). npm (or Yarn, if you prefer) will be used to install the project’s dependencies.

  2. Nest CLI: While not strictly mandatory, using the Nest CLI (@nestjs/cli) makes it simpler to generate new modules, controllers, and services. Install it globally if you want to generate boilerplate code with commands like nest g. Install by running:

    $ npm i -g @nestjs/cli
    
  3. Stripe Account: You’ll need a Stripe account to get an API key.

  4. Basic NestJS Understanding: Some familiarity with NestJS modules, services, and controllers will help. We’ll still explain the code as we go, so if you’re a bit new, you can still keep up.

With these things in place, you’re ready to create a fresh NestJS project or add Stripe to an existing one.

Project Setup

Let’s begin by setting up a new NestJS project. You can do this using the Nest CLI:

$ nest new stripe-nest-tutorial

This command scaffolds a new NestJS project named stripe-nest-tutorial. Next, navigate into that directory:

$ cd stripe-nest-tutorial

Inside your newly created project, you’ll install Stripe and the NestJS Config Module. The Config Module helps with environment variables:

npm install stripe @nestjs/config

At this point, you have a simple NestJS application with a standard structure (e.g., an AppModule, AppController, etc.) and dependencies stripe and @nestjs/config.

Next, you want to create or edit an .env file at the root of the project for our environment variables. In that file, place your Stripe secret key:

STRIPE_API_KEY=sk_test_123456...

⚠️ Note: Don’t commit real secret keys to public repositories.

This environment variable will be read by your NestJS app via @nestjs/config.

Introducing the Stripe Module

Within NestJS, one of the best practices for integrating third-party services is to create a dedicated module. This module can handle all the configuration logic, controllers, and services related to Stripe. That way, you keep the rest of your application’s modules clean and free from payment-specific clutter.

Let’s break down the files that make up our Stripe integration. You’ll have:

  1. stripe.module.ts: A module that sets up Stripe and provides the Stripe API key to the rest of the app.
  2. stripe.service.ts: A service that talks directly to the Stripe SDK. This is where you’ll create payment intents, fetch products, and so on.
  3. stripe.controller.ts: A controller that exposes our Stripe-related endpoints. You can call these endpoints from your frontend or from other parts of your application.

Run the following command in your terminal to generate these files:

$ nest g module stripe && nest g controller stripe && nest g service stripe

Let's now edit these boilerplate files to fit our needs. Edit stripe.module.ts as follows:

import { DynamicModule, Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { StripeController } from './stripe.controller';
import { StripeService } from './stripe.service';

@Module({})
export class StripeModule {
  static forRootAsync(): DynamicModule {
    return {
      module: StripeModule,
      controllers: [StripeController],
      imports: [ConfigModule.forRoot()],
      providers: [
        StripeService,
        {
          provide: 'STRIPE_API_KEY',
          useFactory: async (configService: ConfigService) =>
            configService.get('STRIPE_API_KEY'),
          inject: [ConfigService],
        },
      ],
    };
  }
}

Module Explanation

forRootAsync() is a pattern used in NestJS that allows for asynchronous or dynamic configuration. It’s handy when you need to load environment variables or perform other tasks during initialization.

Once our module is set up, we can import it in our app.module.ts:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { StripeModule } from './stripe/stripe.module';

@Module({
  imports: [StripeModule.forRootAsync()],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

This line ensures that the Stripe module is available to the entire application.

Building the Stripe Service

Now let’s dive into the stripe.service.ts. This is where you’ll see how NestJS interacts with the official Stripe Node library. Here is the code:

import { Inject, Injectable, Logger } from '@nestjs/common';
import Stripe from 'stripe';

@Injectable()
export class StripeService {
  private stripe: Stripe;
  private readonly logger = new Logger(StripeService.name);

  constructor(
    @Inject('STRIPE_API_KEY')
    private readonly apiKey: string,
  ) {
    this.stripe = new Stripe(this.apiKey, {
      apiVersion: '2024-12-18.acacia', // Use latest API version, or "null" for your default
    });
  }

  // Get Products
  async getProducts(): Promise<Stripe.Product[]> {
    try {
      const products = await this.stripe.products.list();
      this.logger.log('Products fetched successfully');
      return products.data;
    } catch (error) {
      this.logger.error('Failed to fetch products', error.stack);
      throw error;
    }
  }

  // Get Customers
  async getCustomers() {
    try {
      const customers = await this.stripe.customers.list({});
      this.logger.log('Customers fetched successfully');
      return customers.data;
    } catch (error) {
      this.logger.error('Failed to fetch products', error.stack);
      throw error;
    }
  }

  // Accept Payments (Create Payment Intent)
  async createPaymentIntent(
    amount: number,
    currency: string,
  ): Promise<Stripe.PaymentIntent> {
    try {
      const paymentIntent = await this.stripe.paymentIntents.create({
        amount,
        currency,
      });
      this.logger.log(
        `PaymentIntent created successfully with amount: ${amount} ${currency}`,
      );
      return paymentIntent;
    } catch (error) {
      this.logger.error('Failed to create PaymentIntent', error.stack);
      throw error;
    }
  }

  // Subscriptions (Create Subscription)
  async createSubscription(
    customerId: string,
    priceId: string,
  ): Promise<Stripe.Subscription> {
    try {
      const subscription = await this.stripe.subscriptions.create({
        customer: customerId,
        items: [{ price: priceId }],
      });
      this.logger.log(
        `Subscription created successfully for customer ${customerId}`,
      );
      return subscription;
    } catch (error) {
      this.logger.error('Failed to create subscription', error.stack);
      throw error;
    }
  }

  // Customer Management (Create Customer)
  async createCustomer(email: string, name: string): Promise<Stripe.Customer> {
    try {
      const customer = await this.stripe.customers.create({ email, name });
      this.logger.log(`Customer created successfully with email: ${email}`);
      return customer;
    } catch (error) {
      this.logger.error('Failed to create customer', error.stack);
      throw error;
    }
  }

  // Product & Pricing Management (Create Product with Price)
  async createProduct(
    name: string,
    description: string,
    price: number,
  ): Promise<Stripe.Product> {
    try {
      const product = await this.stripe.products.create({ name, description });
      await this.stripe.prices.create({
        product: product.id,
        unit_amount: price * 100, // amount in cents
        currency: 'usd',
      });
      this.logger.log(`Product created successfully: ${name}`);
      return product;
    } catch (error) {
      this.logger.error('Failed to create product', error.stack);
      throw error;
    }
  }

  // Refunds (Process Refund)
  async refundPayment(paymentIntentId: string): Promise<Stripe.Refund> {
    try {
      const refund = await this.stripe.refunds.create({
        payment_intent: paymentIntentId,
      });
      this.logger.log(
        `Refund processed successfully for PaymentIntent: ${paymentIntentId}`,
      );
      return refund;
    } catch (error) {
      this.logger.error('Failed to process refund', error.stack);
      throw error;
    }
  }

  // Payment Method Integration (Attach Payment Method)
  async attachPaymentMethod(
    customerId: string,
    paymentMethodId: string,
  ): Promise<void> {
    try {
      await this.stripe.paymentMethods.attach(paymentMethodId, {
        customer: customerId,
      });
      this.logger.log(
        `Payment method ${paymentMethodId} attached to customer ${customerId}`,
      );
    } catch (error) {
      this.logger.error('Failed to attach payment method', error.stack);
      throw error;
    }
  }

  // Reports and Analytics (Retrieve Balance)
  async getBalance(): Promise<Stripe.Balance> {
    try {
      const balance = await this.stripe.balance.retrieve();
      this.logger.log('Balance retrieved successfully');
      return balance;
    } catch (error) {
      this.logger.error('Failed to retrieve balance', error.stack);
      throw error;
    }
  }

  // Payment Links
  async createPaymentLink(priceId: string): Promise<Stripe.PaymentLink> {
    try {
      const paymentLink = await this.stripe.paymentLinks.create({
        line_items: [{ price: priceId, quantity: 1 }],
      });
      this.logger.log('Payment link created successfully');
      return paymentLink;
    } catch (error) {
      this.logger.error('Failed to create payment link', error.stack);
      throw error;
    }
  }
}

Service Explanation

All these methods rely on this.stripe, which is your connected Stripe client. By wrapping them in a service, you can inject this logic wherever you need it inside your application. If you want to add or modify more functionality, you have a centralized place to do so.

Editing the Stripe Controller

Finally, we have the stripe.controller.ts file, which sets up our API routes, replace its code with this:

import { Body, Controller, Get, Post } from '@nestjs/common';
import { StripeService } from './stripe.service';

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

  @Get('products')
  async getProducts() {
    return this.stripeService.getProducts();
  }

  @Get('customers')
  async getCustomers() {
    return this.stripeService.getCustomers();
  }

  @Post('create-payment-intent')
  async createPaymentIntent(@Body() body: { amount: number; currency: string }) {
    const { amount, currency } = body;
    return this.stripeService.createPaymentIntent(amount, currency);
  }

  @Post('subscriptions')
  async createSubscription(@Body() body: { customerId: string; priceId: string }) {
    const { customerId, priceId } = body;
    return this.stripeService.createSubscription(customerId, priceId);
  }

  @Post('customers')
  async createCustomer(@Body() body: { email: string; name: string }) {
    return this.stripeService.createCustomer(body.email, body.name);
  }

  @Post('products')
  async createProduct(@Body() body: { name: string; description: string; price: number }) {
    return this.stripeService.createProduct(body.name, body.description, body.price);
  }

  @Post('refunds')
  async refundPayment(@Body() body: { paymentIntentId: string }) {
    return this.stripeService.refundPayment(body.paymentIntentId);
  }

  @Post('payment-links')
  async createPaymentLink(@Body() body: { priceId: string }) {
    return this.stripeService.createPaymentLink(body.priceId);
  }

  @Get('balance')
  async getBalance() {
    return this.stripeService.getBalance();
  }
}

Controller Explanation

Notice how each route calls the corresponding function in our StripeService. This design keeps your code organized: the controller handles incoming requests, while the service manages the integration with Stripe. If you need to alter business rules or add extra logging, you can do so in the service without messing around in the controller.

Testing Your Routes

By this point, you have all the pieces of a functioning Stripe integration. You can run your NestJS application with:

npm run start:dev

Then, test any of your endpoints with your preferred tool—Postman, cURL, or even a frontend client:

Improving and Expanding

After verifying that the basics work, you can expand your Stripe integration with additional steps:

The arrangement we have now—module, service, and controller—should give you a solid foundation to add all these features without your code becoming messy.


Stripe and NestJS are a powerful duo for projects that require robust, maintainable payment flows.

Check Stripe’s official documentation for more advanced operations.