Your resource for web content, online publishing
and the distribution of digital products.
S M T W T F S
 
 
 
 
 
 
1
 
2
 
3
 
4
 
5
 
6
 
7
 
8
 
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Here’s How to Integrate Stripe with NestJS Like a Pro

DATE POSTED:February 27, 2025

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
  1. Organizing Code: Strategies for placing Stripe logic in dedicated service and controller files for better maintainability.
  2. 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
  1. Stripe Account: You’ll need a Stripe account to get an API key.
  2. 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
  • ConfigModule.forRoot(): Initializes the configuration system in NestJS, letting us use environment variables inside our modules and services.
  • providers: We define two providers here:
  1. StripeService, which contains all the business logic.
  2. An object that provides the actual 'STRIPE_API_KEY'. Notice the use of useFactory and the injection of ConfigService. This is a convenient way to retrieve the STRIPE_API_KEY from our .env file.

\ 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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
  • Constructor: We inject the STRIPE_API_KEY, which was registered in stripe.module.ts. The Stripe object is initialized here with the provided API key.
  • Logger: We’re using Nest’s Logger to give quick feedback about success or failure in each function. This can be replaced with your own logging strategy, but the built-in one is convenient for many use cases.
  • createPaymentIntent: This function is used to initiate a payment. It tells Stripe how much the payment is for and in which currency.
  • createSubscription: Subscriptions allow you to charge customers on a recurring basis. This is especially useful if you’re selling a SaaS product.
  • createCustomer: If you want to save user data and manage recurring billing or one-click payments, you’ll want to create customers in Stripe.
  • createProduct: Products are the items or services that you bill customers for. If you’re building an e-commerce platform, you’ll typically create a product first, then a price for that product.
  • refundPayment: Refunds are inevitable. This function uses the payment_intent identifier to issue a refund.
  • attachPaymentMethod: This function is handy when you want to attach a payment method (e.g., a credit card) to a customer for future billing or subscription creation.
  • getBalance: This is used to retrieve your Stripe balance. Useful for checking how much money is in your account.
  • createPaymentLink: Payment Links are a simplified way to get a ready-to-use link that you can share with your customers. When they open that link, they can handle the entire checkout flow without any extra backend code.

\ 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
  • @Controller('stripe'): This sets up a base route for all endpoints in this controller. That means every route in this file will start with /stripe.

  • @Get('products'): Fetches products. If you hit GET /stripe/products, you’ll get a list of your Stripe products.

  • @Post('create-payment-intent'): Creates a payment intent. Send a JSON body with the amount and currency.

  • @Post('subscriptions'): Creates a subscription. Send a JSON body with a customerId and a priceId.

  • @Post('customers'): Creates a new customer. The body should have an email and name.

  • @Post('products'): Creates a product with a price. The body takes name, description, and price (in dollars).

  • @Post('refunds'): Issues a refund. Send a paymentIntentId in the body to point Stripe to the correct payment.

  • @Post('payment-links'): Generates a payment link (with a pre-built frontend) you can share. Send a priceId in the body.

  • @Get('balance'): Retrieves the current Stripe account balance.

    Payment link

\ 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:

  • Fetching Products:

  • GET http://localhost:3000/stripe/products

  • Creating a Payment Intent:

  • POST http://localhost:3000/stripe/create-payment-intent

  • Body:

    { "amount": 2000, "currency": "usd" }

    If everything’s set up properly, Stripe will respond with a JSON object containing the new payment intent. You can repeat this process for the other routes.

Improving and Expanding

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

\

  • Webhook Handling: Stripe can send events (webhooks) to your server whenever payments succeed or fail, or when a subscription is canceled, and more. Handling these webhooks allows you to automate tasks in your app (e.g., unlocking a feature after a successful payment). You can do this by creating a WebhookController in the same or different module and verifying the signature from Stripe.
  • Security Enhancements: NestJS’s guards, interceptors, and built-in tools can help protect your routes. For example, you might only let admins create products or retrieve the full list of customers. Read this guide to learn how.

\ 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.