Introduction to Stripe with React
Nowadays, a lot of web applications accept online payments. Although this is not straightforward to implement, there are some ready-to-use solutions that we…
June 14, 2021
Nowadays, a lot of web applications accept online payments. Although this is not straightforward to implement, there are some ready-to-use solutions that we can take advantage of. In this article, we look into Stripe. It serves as a payment processing platform that does the heavy lifting for us. To implement it, we use NestJS and React.
Getting up and running with Stripe#
To start using Stripe, we first need to sign up. After verifying our email address, we can go straight to the API keys page.
On the above page, there are two important things. The first of them is the publishable key. We use it on the frontend part of our application. We can safely expose it in our JavaScript code.
The second one is the secret key. We keep it on the backend side and use it to perform API requests to Stripe. It can be used to create charges or perform refunds, for example. Therefore, we need to keep it confidential.
As a best practice, we make sure not to commit neither of the above keys to the repository. We keep them in the environment variables.
If we want to start accepting real payments with Stripe, we need to activate our account. To do that, we need to answer some questions about our business and provide bank details. Since this is optional for development and testing purposes, we can skip it for now.
Using Stripe with NestJS#
If we would intend only to have simple one-time payments, we could use the prebuilt checkout page. In the upcoming articles, we want to implement features such as saving credit cards for later use. Therefore, we implement a simple custom payment flow:
- A user creates an account through our NestJS API. Under the hood, we create a Stripe customer for the user and save the id for later.
- The user provides the details of the credit card through the React application. We send it straight to the Stripe API.
- Stripe API responds with a payment method id. Our frontend app sends it to our NestJS API.
- Our NestJS API gets the request and charges the user using the Stripe API.
There are various ways to integrate NestJS and Stripe. Although there are some ready-to-use libraries, they don’t have many weekly downloads. Therefore, in this article, we implement Stripe into our NestJS application ourselves. Fortunately, this is a straightforward process.
Let’s start by adding some environment variables we will need.
app.module.ts#
import { Module } from "@nestjs/common"
import { ConfigModule } from "@nestjs/config"
import * as Joi from "@hapi/joi"
@Module({
imports: [
ConfigModule.forRoot({
validationSchema: Joi.object({
STRIPE_SECRET_KEY: Joi.string(),
STRIPE_CURRENCY: Joi.string(),
FRONTEND_URL: Joi.string(), // ...
}),
}), // ...
], // ...
})
export class AppModule {}.env#
STRIPE_SECRET_KEY=sk_test_...
STRIPE_CURRENCY=usd
FRONTEND_URL=http://localhost:3500
# ...The STRIPE_SECRET_KEY is the secret key we copied from our Stripe dashboard. Although we could support payments in various currencies, we store the currency in the STRIPE_CURRENCY variable in this simple example.
We want our React application to send authenticated requests to our API. Therefore, we need to set up Cross-Origin Resource Sharing.
main.ts#
import { ConfigService } from "@nestjs/config"
import { NestFactory } from "@nestjs/core"
import { AppModule } from "./app.module"
async function bootstrap() {
const app = await NestFactory.create(AppModule)
const configService = app.get(ConfigService)
app.enableCors({
origin: configService.get("FRONTEND_URL"),
credentials: true,
}) // ...
await app.listen(3000)
}
bootstrap()Setting up Stripe#
We don’t have to make requests to the Stripe API manually. There is a library that can do that for us.
npm install stripeWe need to provide the above package with the secret Stripe key. To do that, let’s create a service.
stripe.service.ts#
import { Injectable } from "@nestjs/common"
import { ConfigService } from "@nestjs/config"
import Stripe from "stripe"
@Injectable()
export default class StripeService {
private stripe: Stripe
constructor(private configService: ConfigService) {
this.stripe = new Stripe(configService.get("STRIPE_SECRET_KEY"), {
apiVersion: "2020-08-27",
})
}
}Above, we use the STRIPE_SECRET_KEY variable we’ve defined before. We also need to define the version of the Stripe API.
Stripe sometimes makes changes to their API that isn’t backward-compatible. To avoid issues, we can define the version of the API we want to use. At the time of writing this article, the current API version is 2020-08-27.
If you want to know more about the changes to the API, check out the API changelog
Creating a customer#
In our application, we want only the authenticated users to be able to make payments. Because of that, we can create a Stripe customer for each of our users. To do that, let’s add the createCustomer method to our StripeService.
stripe.service.ts#
import { Injectable } from "@nestjs/common"
import { ConfigService } from "@nestjs/config"
import Stripe from "stripe"
@Injectable()
export default class StripeService {
private stripe: Stripe
constructor(private configService: ConfigService) {
this.stripe = new Stripe(configService.get("STRIPE_SECRET_KEY"), {
apiVersion: "2020-08-27",
})
}
public async createCustomer(name: string, email: string) {
return this.stripe.customers.create({
name,
email,
})
}
}The stripe.customers.create function calls the Stripe API and returns the data bout the Stripe customer. We need to save it in our database. To do able to do that, let’s modify our UserEntity.
user.entity.ts#
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"
import { Exclude } from "class-transformer"
@Entity()
class User {
@PrimaryGeneratedColumn()
public id: number
@Column({ unique: true })
public email: string
@Column()
public name: string
@Column()
@Exclude()
public password: string
@Column()
public stripeCustomerId: string // ...
}
export default UserNow, we can use all of the above in the UsersService:
users.service.ts#
import { Injectable } from "@nestjs/common"
import { InjectRepository } from "@nestjs/typeorm"
import { Repository } from "typeorm"
import User from "./user.entity"
import CreateUserDto from "./dto/createUser.dto"
import StripeService from "../stripe/stripe.service"
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
private stripeService: StripeService,
) {}
async create(userData: CreateUserDto) {
const stripeCustomer = await this.stripeService.createCustomer(userData.name, userData.email)
const newUser = await this.usersRepository.create({
...userData,
stripeCustomerId: stripeCustomer.id,
})
await this.usersRepository.save(newUser)
return newUser
} // ...
}In the upcoming articles in this series, we’ll be able to use the stripeCustomerId to, for example, save credit cards for the user and retrieve them.
Charging the user#
The last part of our NestJS API is the logic for charging the user. Let’s start with adding the charge method to our StripeService:
stripe.service.ts#
import { Injectable } from "@nestjs/common"
import { ConfigService } from "@nestjs/config"
import Stripe from "stripe"
@Injectable()
export default class StripeService {
private stripe: Stripe
constructor(private configService: ConfigService) {
this.stripe = new Stripe(configService.get("STRIPE_SECRET_KEY"), {
apiVersion: "2020-08-27",
})
}
public async createCustomer(name: string, email: string) {
return this.stripe.customers.create({
name,
email,
})
}
public async charge(amount: number, paymentMethodId: string, customerId: string) {
return this.stripe.paymentIntents.create({
amount,
customer: customerId,
payment_method: paymentMethodId,
currency: this.configService.get("STRIPE_CURRENCY"),
confirm: true,
})
}
}There a few important things happening above:
- the
paymentMethodIdis an id sent by our frontend app after saving the credit card details, - the
customerIdis the Stripe customer id of a user that is making the payment, - the
confirmflag is set to true to indicate that we want to confirm the payment immediately.
Instead of setting the
confirmflag to true, we can also confirm the payment separately.
To use the above logic, let’s create the ChargeController:
charge.controller.ts#
import { Body, Controller, Post, Req, UseGuards } from "@nestjs/common"
import JwtAuthenticationGuard from "../authentication/jwt-authentication.guard"
import CreateChargeDto from "./dto/createCharge.dto"
import RequestWithUser from "../authentication/requestWithUser.interface"
import StripeService from "../stripe/stripe.service"
@Controller("charge")
export default class ChargeController {
constructor(private readonly stripeService: StripeService) {}
@Post()
@UseGuards(JwtAuthenticationGuard)
async createCharge(@Body() charge: CreateChargeDto, @Req() request: RequestWithUser) {
await this.stripeService.charge(
charge.amount,
charge.paymentMethodId,
request.user.stripeCustomerId,
)
}
}createCharge.dto.ts#
import { IsString, IsNotEmpty, IsNumber } from "class-validator"
export class CreateChargeDto {
@IsString()
@IsNotEmpty()
paymentMethodId: string
@IsNumber()
amount: number
}
export default CreateChargeDtoUsing Stripe with React#
To take advantage of all of the backend code we wrote above, we need a React application. In this article, we are using Create React App with TypeScript.
The first step to do is to add the Stripe publishable key to environment variables. While we’re on it, we also add the PORT and the REACT_APP_API_URL variable.
Make sure not to confuse the publishable key with the secret key.
REACT_APP_STRIPE_PUBLISHABLE_KEY=pk_test_...
REACT_APP_API_URL=http://localhost:3000
PORT=3500All custom environment variables need to start with
REACT_APPwhen using Create React App.
Now, we need to install some Stripe-related dependencies.
npm install @stripe/stripe-js @stripe/react-stripe-jsTo integrate Stripe with React, we need to start with providing the publishable key.
App.tsx#
import React, {useEffect} from 'react';
import { Elements } from '@stripe/react-stripe-js';
import { loadStripe } from '@stripe/stripe-js';
import PaymentForm from './PaymentForm';
const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY);
function App() {
return (
<Elements stripe={stripePromise}>
<PaymentForm />
</Elements>
);
}
export default App;Above, you can see that we use the PaymentForm component. Let’s create it:
PaymentForm/index.tsx#
import React from 'react';
import { CardElement } from '@stripe/react-stripe-js';
import usePaymentForm from './usePaymentForm';
const PaymentForm = () => {
const { handleSubmit } = usePaymentForm();
return (
<form onSubmit={handleSubmit}>
<CardElement />
<button>Pay</button>
</form>
);
};
export default PaymentForm;A significant thing to consider is that the Stripe library renders the credit card form in an iframe.
To access the data provided by the user and send it to the Stripe API, we need the useElements hook provided by Stripe. Let’s use it:
import { FormEvent } from "react"
import { CardElement, useElements, useStripe } from "@stripe/react-stripe-js"
function usePaymentForm() {
const stripe = useStripe()
const elements = useElements()
const handleSubmit = async (event: FormEvent) => {
event.preventDefault()
const amountToCharge = 100
const cardElement = elements?.getElement(CardElement)
if (!stripe || !elements || !cardElement) {
return
}
const stripeResponse = await stripe.createPaymentMethod({
type: "card",
card: cardElement,
})
const { error, paymentMethod } = stripeResponse
if (error || !paymentMethod) {
return
}
const paymentMethodId = paymentMethod.id
fetch(`${process.env.REACT_APP_API_URL}/charge`, {
method: "POST",
body: JSON.stringify({
paymentMethodId,
amount: amountToCharge,
}),
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
}
return {
handleSubmit,
}
}
export default usePaymentFormThe above code could use some more graceful error handling. We could also add an impoint for the amount to charge. Feel free to implement it.
In our usePaymentForm hook, we get the data provided by the user by calling elements.getElement(CardElement). We then send it to the Stripe API with the createPaymentMethod method:
stripe.createPaymentMethod({
type: 'card',
card: cardElement
});In response, Stripe sends us the paymentMethod. We send it to our NestJS API to finalize the payment.
We can now go to the payments dashboard to see that our payment is working as intended.
Above you can see that setting
100as the amount means 100 cents, not 100 dollars.
Summary#
In this article, we’ve learned the basics about Stripe. While doing so, we’ve implemented a simple flow where the user provides the credit card details and makes a payment. This included setting up a Stripe account, implementing a NestJS endpoint to charge the user, and creating a simple React application. There is a lot more about Stripe to learn, so stay tuned!