@reyco1/nestjs-stripe
v2.0.1
Published
A NestJS module for Stripe integration supporting one-time payments and subscriptions
Maintainers
Readme
@reyco1/nestjs-stripe
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.
Table of Contents
- @reyco1/nestjs-stripe
Overview 🛠️
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
.envand.env.examplefiles:STRIPE_API_KEY=your_stripe_secret_key STRIPE_API_VERSION=your_stripe_api_version STRIPE_WEBHOOK_SECRET=your_webhook_secret
Features ✨
- 💳 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
Installation 📦
# Install the package
npm install @reyco1/nestjs-stripe
# Run the configuration script (if automatic setup didn't run)
npx @reyco1/nestjs-stripeBasic Usage 💡
Using StripeService (Core Operations)
@Injectable()
export class PaymentService {
constructor(private readonly stripeService: StripeService) {}
async createPayment() {
return this.stripeService.createPaymentIntent({
amount: 1000,
currency: 'usd'
});
}
}Using StripeUtils (Enhanced Data Handling)
@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)
};
}
}Using Raw Stripe Client
@Injectable()
export class PaymentService {
constructor(
@Inject(STRIPE_CLIENT_TOKEN) private readonly stripeClient: Stripe
) {}
async createPayment() {
return this.stripeClient.paymentIntents.create({
amount: 1000,
currency: 'usd'
});
}
}Configuration ⚙️
Module Configuration
// 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 {}Checkout Sessions 🛍️
Payment Checkout
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'
});Subscription Checkout
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'
});Customer Creation Behavior
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
customerIdis provided: Existing customer is used andcustomerCreationis ignored
Configuration Options
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;
}Utility Methods 🛠️
Customer Details
const customerDetails = await stripeUtils.getCustomerDetails(paymentIntent);
// Returns:
{
customerId: string;
email?: string;
name?: string;
phone?: string;
metadata?: Record<string, string>;
}Payment Method Details
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>;
}Refund Information
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>;
}>;
}Subscription Details
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>;
}>;
}Amount Formatting
const formattedAmount = stripeUtils.formatAmount(1000, 'usd');
// Returns: "$10.00"Payment Operations 💳
Creating One-Time Payments
@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)
};
}
}Subscription Management 📅
@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);
}
}Webhook Handling 🎣
1. Add the module to your application
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 {}2. Configure your NestJS application to handle raw body data
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();3. Create services with webhook handlers
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
}
}4. Register your services in a module
import { Module } from '@nestjs/common';
import { SubscriptionService } from './subscription.service';
@Module({
providers: [SubscriptionService],
})
export class SubscriptionsModule {}Features
- Declarative Approach: Use the
@StripeWebhookHandlerdecorator 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.
Best Practices
- 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.
Available Webhook Events
Here are some common Stripe webhook events you might want to handle:
Customer & Subscription Events
customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedcustomer.createdcustomer.updatedcustomer.deleted
Payment & Invoice Events
payment_intent.succeededpayment_intent.payment_failedinvoice.paidinvoice.payment_failedcharge.succeededcharge.failedcharge.refunded
Checkout Events
checkout.session.completedcheckout.session.expired
Check the Stripe API documentation for a complete list of event types.
Connected Accounts 🌐
The Connected Accounts module allows you to create and manage Stripe Connect accounts, enabling platforms to facilitate payments between customers and service providers/merchants.
Basic Usage
@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
};
}
}Adding Bank Accounts
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
});
}Processing Payments for Connected Accounts
// 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' }
);
}Creating Checkout Sessions for Connected Accounts
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
}]
});
}Managing Connected Accounts
// 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');
}Webhook Handling for Connected Accounts
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
}
}Contributing 🤝
Contributions are welcome! Please feel free to submit a Pull Request.
License 📄
MIT
Made with ❤️ by Reyco1
