DEV Community

Cover image for Stripe Subscription Payments With Nest.js and Prisma — Part 1
Ferhat Demir
Ferhat Demir

Posted on

Stripe Subscription Payments With Nest.js and Prisma — Part 1

In this article, I want to share how you can implement a Stripe subscription using Nest.js and Prisma.

First of all, I want to describe the Nest.js and Prisma;

Nest.js is a powerful Node.js framework for building efficient, scalable server-side applications. It supports TypeScript, has a modular architecture, and a dependency injection system for easy maintenance.

Prisma is a modern ORM with a type-safe, auto-generated query builder for interacting with databases. It includes tools like Prisma Client for type-safe database queries and Prisma Migrate for managing schema changes, making it a popular choice for JavaScript and TypeScript projects.

We need a Stripe account for this implementation. Let’s create a new one from here. After creating the account, we will be redirected to the Stripe dashboard. Make sure the Test mode is on. Also, we can skip the setup steps for now.

Then, let’s create Basic and Pro plans and their prices.

Image description

Image description

We have two products, the Basic and the Pro, and two pricing options for each plan: Monthly and Yearly.

Let’s create a new Nest.js application.

npm i -g @nestjs/cli
nest new project-name
Enter fullscreen mode Exit fullscreen mode

After creating a Nest.js application, create a docker-compose.yml file in the main folder of your project:

touch docker-compose.yml
Enter fullscreen mode Exit fullscreen mode

This docker-compose.yml file is a configuration file containing the specifications for running a docker container with PostgreSQL setup inside. Create the following configuration inside the file:

version: '3.8'
services:

  postgres:
    image: postgres:13.5
    restart: always
    environment:
      - POSTGRES_USER=myuser
      - POSTGRES_PASSWORD=mypassword
    volumes:
      - postgres:/var/lib/postgresql/data
    ports:
      - '5432:5432'

volumes:
  postgres:
Enter fullscreen mode Exit fullscreen mode

To start the postgres container, open a new terminal window, and run the following command in the main folder of your project:

docker-compose up
Enter fullscreen mode Exit fullscreen mode

Now that the database is ready, it’s time to set up Prisma!

npm install -D prisma
npx prisma init
Enter fullscreen mode Exit fullscreen mode

This will create a new prisma directory with a schema.prisma file. After that, you should see DATABASE_URL the .env file in the prisma directory. Please update the connection string.

Now, it’s time to define the data models for our application.

#prisma/schema.prisma

model users {
  userId               String   @id @default(uuid())
  firstName            String?
  lastName             String?
  email                String?  @unique
  subscriptionId       String?
  subscription         subscriptions?
  customer             customers?
}

model customers {
  id         String   @id @default(uuid())
  customerId String
  userId     String   @unique
  user       users    @relation(fields: [userId], references: [userId])
}

model products {
  id          String   @id @default(uuid())
  productId   String   @unique
  active      Boolean  @default(true)
  name        String
  description String?
  prices      prices[]
}

model prices {
  id                  String              @id @default(uuid())
  priceId             String              @unique
  productId           String
  product             products            @relation(fields: [productId], references: [id])
  description         String?
  unitAmount          Int                 
  currency            String              
  pricingType         PricingType         // one_time, recurring
  pricingPlanInterval PricingPlanInterval // day, week, month, year
  intervalCount       Int                 
  subscription        subscriptions[]
  type                PlanType            // basic, pro
}
Enter fullscreen mode Exit fullscreen mode

I want to describe some values;

  • unitAmount: The unit amount as a positive integer in the smallest currency unit.

  • currency: Three-letter ISO currency code, in lowercase.

  • pricingType: One of one_time or recurring depending on whether the price is for a one-time purchase or a recurring (subscription) purchase

  • pricingPlanInterval: The frequency at which a subscription is billed. One of day, week, month, or year.

  • intervalCount: The number of intervals between subscription billings. For example, pricingPlanInterval=3, and intervalCount=3 bills every three months.

Let's create the final model and describe some of its values.

#prisma/schema.prisma

model subscriptions {
  subscriptionId         String              @id @default(uuid())
  providerSubscriptionId String?
  userId                 String              @unique
  planType               PlanType            
  user                   users               @relation(fields: [userId], references: [userId])
  status                 SubscriptionStatus? // trialing, active, canceled, incomplete, incomplete_expired, past_due, unpaid, paused
  quantity               Int?
  priceId                String?
  price                  prices?             @relation(fields: [priceId], references: [id])
  cancelAtPeriodEnd      Boolean             @default(false)
  currentPeriodStart     DateTime?
  currentPeriodEnd       DateTime?
  endedAt                DateTime?
  cancelAt               DateTime?
  canceledAt             DateTime?
}
Enter fullscreen mode Exit fullscreen mode
  • cancelAtPeriodEnd: If true, the subscription has been canceled by the user and will be deleted at the end of the billing period. currentPeriodStart: Start of the current period for which the subscription has been invoiced.
  • currentPeriodEnd: At the end of the current period, the subscription has been invoiced. At the end of this period, a new invoice will be created.
  • endedAt: If the subscription has ended, the timestamp is the date the subscription ended.
  • cancelAt: A date at which the subscription will automatically get canceled.
  • canceledAt: If the subscription has been canceled, the date of that cancellation. If the subscription was canceled with cancelAtPeriodEnd, canceledAt will still reflect the date of the initial cancellation request, not the end of the subscription period when the subscription is automatically moved to a canceled state.

We defined the schema. To generate and execute the migration, run the following command in the terminal:

npx prisma migrate dev --name "init"
Enter fullscreen mode Exit fullscreen mode

We created the tables and their fields, but they are currently empty. Let’s create a seed file and populate the users, products, and prices tables.

#prisma/seed.ts

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

async function main() {
  await prisma.users.create({
    data: {
      firstName: 'Ferhat',
      lastName: 'Demir',
      email: 'edemirferhat@gmail.com',
    },
  });

  await prisma.products.createMany({
    data: [
      {
        productId: 'prod_QSkslDcBhj8y2Z',
        name: 'Basic Plan',
      },
      {
        productId: 'prod_QSktVsKo34GHYN',
        name: 'Pro Plan',
      },
    ],
  });

  // fetch the created products to get their IDs
  const products = await prisma.products.findMany({
    where: {
      productId: { in: ['prod_QSkslDcBhj8y2Z', 'prod_QSktVsKo34GHYN'] },
    },
  });

  const productIdMap = products.reduce((map, product) => {
    map[product.productId] = product.id;
    return map;
  }, {});

  // create prices next
  await prisma.prices.createMany({
    data: [
      {
        priceId: 'price_1PbpgJKWfbR45IR7mYwUUMT9',
        unitAmount: 10,
        currency: 'usd',
        productId: productIdMap['prod_QSkslDcBhj8y2Z'],
        intervalCount: 1,
        pricingPlanInterval: 'month',
        pricingType: 'recurring',
        type: 'basic',
      },
      {
        priceId: 'price_1PbprLKWfbR45IR7HCZf44op',
        unitAmount: 100,
        currency: 'usd',
        productId: productIdMap['prod_QSktVsKo34GHYN'],
        intervalCount: 1,
        pricingPlanInterval: 'year',
        pricingType: 'recurring',
        type: 'basic',
      },
      {
        priceId: 'price_1PbPqgKWfbR45IR7L4V0Y7RL',
        unitAmount: 20,
        currency: 'usd',
        productId: productIdMap['prod_QSktvsKo34GHYN'],
        intervalCount: 1,
        pricingPlanInterval: 'month',
        pricingType: 'recurring',
        type: 'pro',
      },
      {
        priceId: 'price_1PbpQVlWfbR45IR7TBywWkkO',
        unitAmount: 200,
        currency: 'usd',
        productId: productIdMap['prod_QSktVsKo34GHYN'],
        intervalCount: 1,
        pricingPlanInterval: 'year',
        pricingType: 'recurring',
        type: 'pro',
      },
    ],
  });
}

// execute the main function
main()
  .catch((e) => {
    console.error(e);
    process.exit(1);
  })
  .finally(async () => {
    // close Prisma Client at the end
    await prisma.$disconnect();
  });
Enter fullscreen mode Exit fullscreen mode

To execute the seed file, we should define the command on package.json the file.

#package.json

"scripts": {
  // ...
},
"dependencies": {
  // ...
},
"devDependencies": {
  // ...
},
"prisma": {
  "seed": "ts-node prisma/seed.ts"
}
Enter fullscreen mode Exit fullscreen mode

Execute seeding with the following command:

npx prisma db seed
Enter fullscreen mode Exit fullscreen mode

We need to create the necessary modules: Stripe and Prisma. We must also create an endpoint to listen to Stripe payment events.

import { Controller, Post, Req } from '@nestjs/common';
import { StripeService } from './stripe.service';

@Controller('stripe')
export class StripeController {
  constructor(private readonly stripeService: StripeService) {}

  @Post('events')
  async handleEvents(@Req() request) {
    console.log('Received a new event', request.body.type);
  }
}
Enter fullscreen mode Exit fullscreen mode

To efficiently develop and debug in the local environment, we need to install Stripe CLI.

brew install stripe/stripe-cli/stripe
stripe login
Enter fullscreen mode Exit fullscreen mode

Forward events to our endpoint. Then, trigger an event to verify that it is working correctly.

stripe listen --forward-to localhost:3000/stripe/events
stripe trigger payment_intent.succeeded
Enter fullscreen mode Exit fullscreen mode

Now, we should see the logs on the console.

Let's start implementing the business logic and updating our data models.

Top comments (1)

Collapse
 
peterlestylo profile image
peter

Greetings, any chance you've got a github repo with the code?