How to Add Stripe Payments to NestJS: Complete 2026 Guide
Why Stripe + NestJS?
Stripe is the default payment processor for SaaS. NestJS gives you the structure to integrate it cleanly — modules, services, guards, and decorators keep your payment logic organized and testable.
This guide walks through a complete Stripe subscription integration. By the end, you'll have checkout sessions, webhook handling, and subscription status synced to your database.
Step 1: Install Dependencies
npm install stripe
npm install @nestjs/config
You'll need your Stripe secret key and webhook signing secret. Add them to your .env:
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_ID=price_...
Cloudrix SaaS Starter ships with this pre-configured and tested.
Skip weeks of boilerplate — auth, payments, multi-tenancy, and deployment included out of the box.
Try the live demo →Step 2: Create the Stripe Module
NestJS modules keep things encapsulated. Create a dedicated Stripe module:
// stripe.module.ts
@Module({
imports: [ConfigModule],
providers: [StripeService],
controllers: [StripeController],
exports: [StripeService],
})
export class StripeModule {}
Step 3: Initialize the Stripe Client
Wrap the Stripe SDK in a service so you can inject it anywhere:
// stripe.service.ts
@Injectable()
export class StripeService {
private stripe: Stripe;
constructor(private config: ConfigService) {
this.stripe = new Stripe(this.config.get('STRIPE_SECRET_KEY'), {
apiVersion: '2025-12-18.acacia',
});
}
async createCheckoutSession(userId: string, priceId: string) {
return this.stripe.checkout.sessions.create({
mode: 'subscription',
payment_method_types: ['card'],
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${this.config.get('APP_URL')}/billing?success=true`,
cancel_url: `${this.config.get('APP_URL')}/billing?canceled=true`,
metadata: { userId },
});
}
}
Step 4: Handle Webhooks
Webhooks are where most Stripe integrations break. You need to verify the signature and handle events idempotently:
// stripe.controller.ts
@Controller('stripe')
export class StripeController {
constructor(private stripeService: StripeService) {}
@Post('webhook')
async handleWebhook(
@Req() req: RawBodyRequest<Request>,
@Headers('stripe-signature') signature: string,
) {
const event = this.stripeService.constructEvent(
req.rawBody,
signature,
);
switch (event.type) {
case 'checkout.session.completed':
await this.stripeService.handleCheckoutComplete(event.data.object);
break;
case 'customer.subscription.updated':
await this.stripeService.handleSubscriptionUpdate(event.data.object);
break;
case 'customer.subscription.deleted':
await this.stripeService.handleSubscriptionCancel(event.data.object);
break;
}
return { received: true };
}
}
Critical: You must use the raw request body for signature verification. NestJS parses JSON by default, which corrupts the signature check. Configure a raw body parser for the webhook route.
Step 5: Sync Subscription Status
Store the Stripe subscription ID and status in your database. Update it on every webhook event:
async handleSubscriptionUpdate(subscription: Stripe.Subscription) {
await this.userRepository.update(
{ stripeCustomerId: subscription.customer as string },
{
subscriptionStatus: subscription.status,
subscriptionId: subscription.id,
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
},
);
}
Step 6: Guard Premium Routes
Create a subscription guard that checks the user's plan before allowing access:
@Injectable()
export class SubscriptionGuard implements CanActivate {
constructor(private userService: UserService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const user = await this.userService.findById(request.user.id);
return user.subscriptionStatus === 'active';
}
}
Use it on any controller:
@UseGuards(JwtAuthGuard, SubscriptionGuard)
@Get('premium-data')
getPremiumData() {
return this.premiumService.getData();
}
Common Pitfalls
SaaS Starter handles all of these edge cases. See the Stripe payments feature for details.
- Raw body parsing — Without the raw body, webhook signature verification fails silently. Always configure
rawBody: truein your NestJS app bootstrap. - Idempotency — Stripe may send the same webhook multiple times. Use the event ID to deduplicate.
- Test vs. live keys — Use Stripe CLI (
stripe listen --forward-to localhost:3000/stripe/webhook) during development. - Subscription states — Handle
past_due,canceled, andunpaidseparately. Don't just check foractive.
Skip the Setup
Everything above — checkout sessions, webhook handling, subscription guards, status syncing, Stripe CLI integration — is already built and tested in SaaS Starter Pro. The integration handles edge cases like proration, trial periods, and failed payment recovery that would take another week to implement from scratch.
Get Started Today
All of these patterns are fully implemented in SaaS Starter. Skip 2-6 weeks of setup — auth, payments, multi-tenancy, deployment all done.
- Free version: GitHub
- Pro ($249): Get SaaS Starter Pro