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.
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
After creating a Nest.js application, create a docker-compose.yml
file in the main folder of your project:
touch docker-compose.yml
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:
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
Now that the database is ready, it’s time to set up Prisma!
npm install -D prisma
npx prisma init
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
}
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
orrecurring
depending on whether the price is for a one-time purchase or a recurring (subscription) purchasepricingPlanInterval: The frequency at which a subscription is billed. One of
day,
week,
month
, oryear.
intervalCount: The number of intervals between subscription billings. For example,
pricingPlanInterval=3
, andintervalCount=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?
}
- 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"
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();
});
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"
}
Execute seeding with the following command:
npx prisma db seed
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);
}
}
To efficiently develop and debug in the local environment, we need to install Stripe CLI.
brew install stripe/stripe-cli/stripe
stripe login
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
Now, we should see the logs on the console.
Let's start implementing the business logic and updating our data models.
Top comments (1)
Greetings, any chance you've got a github repo with the code?