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\ If that sounds interesting to you, let’s get building!
What You’ll NeedBefore you write any code, ensure you have the following:
\ With these things in place, you’re ready to create a fresh NestJS project or add Stripe to an existing one.
Project SetupLet’s begin by setting up a new NestJS project. You can do this using the Nest CLI:
$ nest new stripe-nest-tutorialThis command scaffolds a new NestJS project named stripe-nest-tutorial. Next, navigate into that directory:
$ cd stripe-nest-tutorialInside your newly created project, you’ll install Stripe and the NestJS Config Module. The Config Module helps with environment variables:
npm install stripe @nestjs/configAt 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 ModuleWithin 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:
\ 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 ServiceNow 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\ 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 ControllerFinally, 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.
\ 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 RoutesBy 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.
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.
All Rights Reserved. Copyright , Central Coast Communications, Inc.