A powerful NestJS module for Stripe integration that supports both one-time payments and subscriptions. This package provides a seamless way to integrate Stripe payment processing into your NestJS application.
- @reyco1/nestjs-stripe
- Table of Contents
- Overview π οΈ
- Features β¨
- Installation π¦
- Basic Usage π‘
- Configuration βοΈ
- Checkout Sessions ποΈ
- Utility Methods π οΈ
- Payment Operations π³
- Subscription Management π
- Webhook Handling π£
- Features
- Best Practices
- Available Webhook Events
- Connected Accounts π
- Contributing π€
- License π
When installed, this package will:
-
Add required imports to your
app.module.ts
:- ConfigService from @nestjs/config
- StripeModule from @reyco1/nestjs-stripe
-
Configure the StripeModule with async configuration using ConfigService
-
Add necessary environment variables to your
.env
and.env.example
files:STRIPE_API_KEY=your_stripe_secret_key STRIPE_API_VERSION=your_stripe_api_version STRIPE_WEBHOOK_SECRET=your_webhook_secret
- π³ One-time payment processing
- π Subscription management
- ποΈ Stripe Checkout integration
- π₯ Customer management
- π£ Webhook handling
- π TypeScript support
- π Auto-configuration setup
- π§ Environment variables management
- π οΈ Comprehensive utility methods
- π Type-safe interfaces
- πͺ Enhanced data handling and validation
- π Detailed payment information extraction
- π Secure webhook processing
# Install the package
npm install @reyco1/nestjs-stripe
# Run the configuration script (if automatic setup didn't run)
npx @reyco1/nestjs-stripe
@Injectable()
export class PaymentService {
constructor(private readonly stripeService: StripeService) {}
async createPayment() {
return this.stripeService.createPaymentIntent({
amount: 1000,
currency: 'usd'
});
}
}
@Injectable()
export class PaymentService {
constructor(private readonly stripeUtils: StripeUtils) {}
async getPaymentDetails(paymentIntent: Stripe.PaymentIntent) {
const [customerDetails, paymentMethod, refundInfo] = await Promise.all([
this.stripeUtils.getCustomerDetails(paymentIntent),
this.stripeUtils.getPaymentMethodDetails(paymentIntent),
this.stripeUtils.getRefundInfo(paymentIntent)
]);
return {
customer: customerDetails,
payment: paymentMethod,
refunds: refundInfo,
amount: this.stripeUtils.formatAmount(paymentIntent.amount)
};
}
}
@Injectable()
export class PaymentService {
constructor(
@Inject(STRIPE_CLIENT_TOKEN) private readonly stripeClient: Stripe
) {}
async createPayment() {
return this.stripeClient.paymentIntents.create({
amount: 1000,
currency: 'usd'
});
}
}
// app.module.ts
import { StripeModule } from '@reyco1/nestjs-stripe';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot(),
StripeModule.forRootAsync({
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
apiKey: configService.get('STRIPE_API_KEY'),
apiVersion: configService.get('STRIPE_API_VERSION'),
webhookSecret: configService.get('STRIPE_WEBHOOK_SECRET'),
}),
}),
],
})
export class AppModule {}
Create one-time payment checkout sessions:
const session = await stripeService.createPaymentCheckoutSession({
successUrl: 'https://example.com/success',
cancelUrl: 'https://example.com/cancel',
lineItems: [{
price: 'price_H5ggYwtDq4fbrJ',
quantity: 1
}],
// Or create a product on the fly:
// lineItems: [{
// name: 'T-shirt',
// amount: 2000,
// currency: 'usd',
// quantity: 1
// }],
paymentMethodTypes: ['card'],
shippingAddressCollection: {
allowed_countries: ['US', 'CA']
},
billingAddressCollection: 'required',
customerCreation: 'if_required'
});
Create subscription checkout sessions:
const session = await stripeService.createSubscriptionCheckoutSession({
successUrl: 'https://example.com/success',
cancelUrl: 'https://example.com/cancel',
lineItems: [{
price: 'price_H5ggYwtDq4fbrJ', // recurring price ID
quantity: 1
}],
paymentMethodTypes: ['card'],
trialPeriodDays: 14,
subscriptionData: {
description: 'Premium Plan Subscription',
metadata: {
plan: 'premium'
}
},
customerCreation: 'if_required'
});
The customer creation behavior in checkout sessions depends on how you configure the customerId
and customerCreation
parameters:
- Using Existing Customer
await stripeService.createPaymentCheckoutSession({
customerId: 'cus_123...', // Will use this customer
customerCreation: 'always', // This will be ignored
// ... other params
});
- New Customer for One-time Payment
await stripeService.createPaymentCheckoutSession({
customerCreation: 'always', // Will create new customer
// ... other params
});
- New Customer for Subscription
await stripeService.createSubscriptionCheckoutSession({
customerCreation: 'if_required', // Will create customer since needed for subscriptions
// ... other params
});
- Default Behavior
- For one-time payments: Customer is only created if specifically requested
- For subscriptions: Customer is always created if not provided
- When
customerId
is provided: Existing customer is used andcustomerCreation
is ignored
Common configuration options for checkout sessions:
interface CheckoutSessionOptions {
// Required parameters
successUrl: string; // Redirect after successful payment
cancelUrl: string; // Redirect if customer cancels
lineItems: LineItem[]; // Products/prices to charge
// Customer handling
customerId?: string; // Existing customer ID
customerEmail?: string; // Pre-fill customer email
customerCreation?: 'always' | 'if_required';
// Payment configuration
paymentMethodTypes?: PaymentMethodType[]; // e.g., ['card', 'sepa_debit']
allowPromotionCodes?: boolean;
// Address collection
billingAddressCollection?: 'required' | 'auto';
shippingAddressCollection?: {
allowed_countries: string[]; // e.g., ['US', 'CA']
};
// Customization
locale?: string; // e.g., 'auto' or 'en'
submitType?: 'auto' | 'pay' | 'book' | 'donate';
// Additional data
metadata?: Record<string, string | number>;
clientReferenceId?: string;
}
const customerDetails = await stripeUtils.getCustomerDetails(paymentIntent);
// Returns:
{
customerId: string;
email?: string;
name?: string;
phone?: string;
metadata?: Record<string, string>;
}
const paymentMethod = await stripeUtils.getPaymentMethodDetails(paymentIntent);
// Returns:
{
id?: string;
type?: string;
last4?: string;
brand?: string;
expMonth?: number;
expYear?: number;
billingDetails?: {
name?: string;
email?: string;
phone?: string;
address?: Stripe.Address;
};
metadata?: Record<string, string>;
}
const refundInfo = await stripeUtils.getRefundInfo(paymentIntent);
// Returns:
{
refunded: boolean;
refundedAmount?: number;
refundCount?: number;
refunds?: Array<{
id: string;
amount: number;
status: string;
reason?: string;
created: Date;
metadata?: Record<string, string>;
}>;
}
const subscription = await stripeUtils.getSubscriptionDetails(subscriptionId);
// Returns:
{
id: string;
status: string;
currentPeriodStart: Date;
currentPeriodEnd: Date;
trialStart?: Date;
trialEnd?: Date;
cancelAt?: Date;
canceledAt?: Date;
endedAt?: Date;
metadata?: Record<string, string>;
items?: Array<{
id: string;
priceId: string;
quantity?: number;
metadata?: Record<string, string>;
}>;
}
const formattedAmount = stripeUtils.formatAmount(1000, 'usd');
// Returns: "$10.00"
@Injectable()
export class PaymentService {
constructor(
private readonly stripeService: StripeService,
private readonly stripeUtils: StripeUtils
) {}
async createPayment() {
const payment = await this.stripeService.createPaymentIntent({
amount: 1000,
currency: 'usd',
metadata: {
orderId: 'ORDER_123'
}
});
// Get comprehensive payment details
const details = await this.stripeUtils.getPaymentMethodDetails(payment);
const customer = await this.stripeUtils.getCustomerDetails(payment);
return {
payment,
details,
customer,
formattedAmount: this.stripeUtils.formatAmount(payment.amount)
};
}
}
@Injectable()
export class SubscriptionService {
constructor(
private readonly stripeService: StripeService,
private readonly stripeUtils: StripeUtils
) {}
async createSubscription(customerId: string, priceId: string) {
const subscription = await this.stripeService.createSubscription({
customerId,
priceId,
metadata: {
plan: 'premium'
}
});
return this.stripeUtils.getSubscriptionDetails(subscription.id);
}
async cancelSubscription(subscriptionId: string) {
return this.stripeService.cancelSubscription(subscriptionId);
}
}
In your app.module.ts
(or appropriate module):
import { Module } from '@nestjs/common';
import { StripeModule, StripeWebhookModule } from '@reyco1/nestjs-stripe';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot(),
StripeModule.forRootAsync({
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
apiKey: configService.get('STRIPE_API_KEY'),
apiVersion: configService.get('STRIPE_API_VERSION'),
webhookSecret: configService.get('STRIPE_WEBHOOK_SECRET'),
}),
}),
StripeWebhookModule.forRoot(),
// ... other modules
],
})
export class AppModule {}
In your main.ts
:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as bodyParser from 'body-parser';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Important: Configure raw body parser for Stripe webhooks
app.use(
bodyParser.json({
verify: (req: any, res, buf) => {
if (req.originalUrl.startsWith('/stripe/webhook')) {
req.rawBody = buf;
}
},
})
);
await app.listen(3000);
}
bootstrap();
Create services with methods decorated with @StripeWebhookHandler
:
import { Injectable, Logger } from '@nestjs/common';
import { StripeWebhookHandler } from '@reyco1/nestjs-stripe';
import Stripe from 'stripe';
@Injectable()
export class SubscriptionService {
private readonly logger = new Logger(SubscriptionService.name);
@StripeWebhookHandler('customer.subscription.created')
async handleSubscriptionCreated(event: Stripe.Event): Promise<void> {
const subscription = event.data.object as Stripe.Subscription;
// Process subscription creation
}
@StripeWebhookHandler('customer.subscription.updated')
async handleSubscriptionUpdate(event: Stripe.Event): Promise<void> {
const subscription = event.data.object as Stripe.Subscription;
// Process subscription update
}
@StripeWebhookHandler('customer.subscription.deleted')
async handleSubscriptionDelete(event: Stripe.Event): Promise<void> {
const subscription = event.data.object as Stripe.Subscription;
// Process subscription deletion
}
}
import { Module } from '@nestjs/common';
import { SubscriptionService } from './subscription.service';
@Module({
providers: [SubscriptionService],
})
export class SubscriptionsModule {}
- Declarative Approach: Use the
@StripeWebhookHandler
decorator to specify which methods handle which Stripe events. - Automatic Discovery: The module automatically discovers and registers all webhook handlers during application bootstrap.
- Multiple Handlers: Multiple methods can handle the same event type.
- Type Safety: Fully typed with TypeScript, leveraging Stripe's TypeScript definitions.
- Error Handling: Built-in error handling with detailed logging.
- Signature Verification: Automatically verifies Stripe webhook signatures.
- Service Organization: Group related webhook handlers in dedicated services (e.g.,
SubscriptionService
,PaymentService
). - Error Handling: Add try/catch blocks in your handlers to gracefully handle errors.
- Idempotency: Implement idempotency checks to handle potential duplicate webhook events from Stripe.
- Testing: Use Stripe's webhook testing tools to simulate webhook events.
Here are some common Stripe webhook events you might want to handle:
customer.subscription.created
customer.subscription.updated
customer.subscription.deleted
customer.created
customer.updated
customer.deleted
payment_intent.succeeded
payment_intent.payment_failed
invoice.paid
invoice.payment_failed
charge.succeeded
charge.failed
charge.refunded
checkout.session.completed
checkout.session.expired
Check the Stripe API documentation for a complete list of event types.
The Connected Accounts module allows you to create and manage Stripe Connect accounts, enabling platforms to facilitate payments between customers and service providers/merchants.
@Injectable()
export class MerchantService {
constructor(private readonly connectedAccountsService: ConnectedAccountsService) {}
async onboardMerchant(merchantData: CreateConnectedAccountDto) {
// 1. Create a connected account
const account = await this.connectedAccountsService.createConnectedAccount({
email: merchantData.email,
country: merchantData.country,
capabilities: {
card_payments: { requested: true },
transfers: { requested: true }
}
});
// 2. Generate an onboarding link
const accountLink = await this.connectedAccountsService.createAccountLink({
accountId: account.id,
refreshUrl: 'https://example.com/onboarding/refresh',
returnUrl: 'https://example.com/onboarding/complete',
type: 'account_onboarding'
});
return {
accountId: account.id,
onboardingUrl: accountLink.url
};
}
}
async addBankAccount(accountId: string, bankData: CreateBankAccountDto) {
return this.connectedAccountsService.createBankAccount(accountId, {
country: bankData.country,
currency: bankData.currency,
accountNumber: bankData.accountNumber,
routingNumber: bankData.routingNumber,
accountHolderName: bankData.accountHolderName
});
}
// Create a payment that automatically transfers funds to the connected account
async processPayment(accountId: string, amount: number) {
return this.connectedAccountsService.createPaymentIntent(
amount,
'usd',
accountId,
150 // $1.50 platform fee
);
}
// Create a transfer to a connected account
async transferFunds(accountId: string, amount: number) {
return this.connectedAccountsService.createTransfer(
amount,
'usd',
accountId,
{ description: 'Monthly payout' }
);
}
async createConnectedCheckout(accountId: string) {
return this.connectedAccountsService.createConnectedAccountCheckoutSession({
connectedAccountId: accountId,
applicationFeeAmount: 250, // $2.50 platform fee
successUrl: 'https://example.com/success',
cancelUrl: 'https://example.com/cancel',
lineItems: [{
name: 'Service fee',
amount: 5000, // $50.00
currency: 'usd',
quantity: 1
}]
});
}
// List all connected accounts
async getAllMerchants(limit = 10, startingAfter?: string) {
return this.connectedAccountsService.listConnectedAccounts(limit, startingAfter);
}
// Update a connected account
async updateMerchant(accountId: string, data: Partial<CreateConnectedAccountDto>) {
return this.connectedAccountsService.updateConnectedAccount(accountId, data);
}
// Create a payout for a connected account
async createPayout(accountId: string, amount: number) {
return this.connectedAccountsService.createPayout(accountId, amount, 'usd');
}
Create handlers for important Connect events:
@Injectable()
export class ConnectedAccountWebhookHandler {
private readonly logger = new Logger(ConnectedAccountWebhookHandler.name);
@StripeWebhookHandler('account.updated')
async handleAccountUpdate(event: Stripe.Event): Promise<void> {
const account = event.data.object as Stripe.Account;
// Check if account is now fully onboarded
if (account.charges_enabled && account.payouts_enabled) {
// Update merchant status in your database
}
}
@StripeWebhookHandler('account.application.deauthorized')
async handleAccountDeauthorized(event: Stripe.Event): Promise<void> {
const application = event.data.object as any;
// Handle when a connected account removes your platform's access
}
}
Contributions are welcome! Please feel free to submit a Pull Request.
MIT
Made with β€οΈ by Reyco1