Skip to content
← Back to blog
NestJSStripePaymentsTutorial

How to Add Stripe Payments to NestJS: Complete 2026 Guide

Firas Sayah·May 28, 2026·6 min read

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.

  1. Raw body parsing — Without the raw body, webhook signature verification fails silently. Always configure rawBody: true in your NestJS app bootstrap.
  2. Idempotency — Stripe may send the same webhook multiple times. Use the event ID to deduplicate.
  3. Test vs. live keys — Use Stripe CLI (stripe listen --forward-to localhost:3000/stripe/webhook) during development.
  4. Subscription states — Handle past_due, canceled, and unpaid separately. Don't just check for active.

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.

F

Firas Sayah

Senior Software Engineer

Full-stack developer with 5+ years building production SaaS applications with NestJS and Angular.