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.
1import { Module } from '@nestjs/common';
2import { ConfigModule } from '@nestjs/config';
3import * as Joi from '@hapi/joi';
4
5@Module({
6 imports: [
7 ConfigModule.forRoot({
8 validationSchema: Joi.object({
9 STRIPE_SECRET_KEY: Joi.string(),
10 STRIPE_CURRENCY: Joi.string(),
11 FRONTEND_URL: Joi.string(),
12 // ...
13 })
14 }),
15 // ...
16 ],
17 // ...
18})
19export class AppModule {}1STRIPE_SECRET_KEY=sk_test_...
2STRIPE_CURRENCY=usd
3FRONTEND_URL=http://localhost:3500
4
5# ...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.
If you want to know more about CORS, check out Cross-Origin Resource Sharing. Avoiding Access-Control-Allow-Origin CORS error
1import { NestFactory } from '@nestjs/core';
2import { AppModule } from './app.module';
3import { ConfigService } from '@nestjs/config';
4
5async function bootstrap() {
6 const app = await NestFactory.create(AppModule);
7
8 const configService = app.get(ConfigService);
9
10 app.enableCors({
11 origin: configService.get('FRONTEND_URL'),
12 credentials: true
13 });
14
15 // ...
16
17 await app.listen(3000);
18}
19bootstrap();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.
1npm install stripeWe need to provide the above package with the secret Stripe key. To do that, let’s create a service.
1import { Injectable } from '@nestjs/common';
2import { ConfigService } from '@nestjs/config';
3import Stripe from 'stripe';
4
5@Injectable()
6export default class StripeService {
7 private stripe: Stripe;
8
9 constructor(
10 private configService: ConfigService
11 ) {
12 this.stripe = new Stripe(configService.get('STRIPE_SECRET_KEY'), {
13 apiVersion: '2020-08-27',
14 });
15 }
16}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.
1import { Injectable } from '@nestjs/common';
2import { ConfigService } from '@nestjs/config';
3import Stripe from 'stripe';
4
5@Injectable()
6export default class StripeService {
7 private stripe: Stripe;
8
9 constructor(
10 private configService: ConfigService
11 ) {
12 this.stripe = new Stripe(configService.get('STRIPE_SECRET_KEY'), {
13 apiVersion: '2020-08-27',
14 });
15 }
16
17 public async createCustomer(name: string, email: string) {
18 return this.stripe.customers.create({
19 name,
20 email
21 });
22 }
23}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.
1import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
2import { Exclude } from 'class-transformer';
3
4@Entity()
5class User {
6 @PrimaryGeneratedColumn()
7 public id: number;
8
9 @Column({ unique: true })
10 public email: string;
11
12 @Column()
13 public name: string;
14
15 @Column()
16 @Exclude()
17 public password: string;
18
19 @Column()
20 public stripeCustomerId: string;
21
22 // ...
23}
24
25export default User;Now, we can use all of the above in the UsersService:
1import { Injectable } from '@nestjs/common';
2import { InjectRepository } from '@nestjs/typeorm';
3import { Repository } from 'typeorm';
4import User from './user.entity';
5import CreateUserDto from './dto/createUser.dto';
6import StripeService from '../stripe/stripe.service';
7
8@Injectable()
9export class UsersService {
10 constructor(
11 @InjectRepository(User)
12 private usersRepository: Repository<User>,
13 private stripeService: StripeService
14 ) {}
15
16 async create(userData: CreateUserDto) {
17 const stripeCustomer = await this.stripeService.createCustomer(userData.name, userData.email);
18
19 const newUser = await this.usersRepository.create({
20 ...userData,
21 stripeCustomerId: stripeCustomer.id
22 });
23 await this.usersRepository.save(newUser);
24 return newUser;
25 }
26
27 // ...
28}If you want to know more about authentication, check out API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies
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:
1import { Injectable } from '@nestjs/common';
2import { ConfigService } from '@nestjs/config';
3import Stripe from 'stripe';
4
5@Injectable()
6export default class StripeService {
7 private stripe: Stripe;
8
9 constructor(
10 private configService: ConfigService
11 ) {
12 this.stripe = new Stripe(configService.get('STRIPE_SECRET_KEY'), {
13 apiVersion: '2020-08-27',
14 });
15 }
16
17 public async createCustomer(name: string, email: string) {
18 return this.stripe.customers.create({
19 name,
20 email
21 });
22 }
23
24 public async charge(amount: number, paymentMethodId: string, customerId: string) {
25 return this.stripe.paymentIntents.create({
26 amount,
27 customer: customerId,
28 payment_method: paymentMethodId,
29 currency: this.configService.get('STRIPE_CURRENCY'),
30 confirm: true
31 })
32 }
33}There a few important things happening above:
- the paymentMethodId is an id sent by our frontend app after saving the credit card details,
- the customerId is the Stripe customer id of a user that is making the payment,
- the confirm flag is set to true to indicate that we want to confirm the payment immediately.
Instead of setting the confirm flag to true, we can also confirm the payment separately.
To use the above logic, let’s create the ChargeController:
1import { Body, Controller, Post, Req, UseGuards } from '@nestjs/common';
2import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard';
3import CreateChargeDto from './dto/createCharge.dto';
4import RequestWithUser from '../authentication/requestWithUser.interface';
5import StripeService from '../stripe/stripe.service';
6
7@Controller('charge')
8export default class ChargeController {
9 constructor(
10 private readonly stripeService: StripeService
11 ) {}
12
13 @Post()
14 @UseGuards(JwtAuthenticationGuard)
15 async createCharge(@Body() charge: CreateChargeDto, @Req() request: RequestWithUser) {
16 await this.stripeService.charge(charge.amount, charge.paymentMethodId, request.user.stripeCustomerId);
17 }
18}1import { IsString, IsNotEmpty, IsNumber } from 'class-validator';
2
3export class CreateChargeDto {
4 @IsString()
5 @IsNotEmpty()
6 paymentMethodId: string;
7
8 @IsNumber()
9 amount: number;
10}
11
12export default CreateChargeDto;Using 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.
In this article, we assume that the user is already authenticated. If you want to know more about authenticating with JWT, check out API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies
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.
1REACT_APP_STRIPE_PUBLISHABLE_KEY=pk_test_...
2REACT_APP_API_URL=http://localhost:3000
3PORT=3500All custom environment variables need to start with REACT_APP when using Create React App.
Now, we need to install some Stripe-related dependencies.
1npm install @stripe/stripe-js @stripe/react-stripe-jsTo integrate Stripe with React, we need to start with providing the publishable key.
1import React, {useEffect} from 'react';
2import { Elements } from '@stripe/react-stripe-js';
3import { loadStripe } from '@stripe/stripe-js';
4import PaymentForm from './PaymentForm';
5
6const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY);
7
8function App() {
9 return (
10 <Elements stripe={stripePromise}>
11 <PaymentForm />
12 </Elements>
13 );
14}
15
16export default App;Above, you can see that we use the PaymentForm component. Let’s create it:
1import React from 'react';
2import { CardElement } from '@stripe/react-stripe-js';
3import usePaymentForm from './usePaymentForm';
4
5const PaymentForm = () => {
6 const { handleSubmit } = usePaymentForm();
7
8 return (
9 <form onSubmit={handleSubmit}>
10 <CardElement />
11 <button>Pay</button>
12 </form>
13 );
14};
15
16export default PaymentForm;I like to keep my template separate from the logic to maintain the separation of concerns. If you want to know more, check out JavaScript design patterns #3. The Facade pattern and applying it to React Hooks
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:
1import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
2import { FormEvent } from 'react';
3
4function usePaymentForm() {
5 const stripe = useStripe();
6 const elements = useElements();
7
8 const handleSubmit = async (event: FormEvent) => {
9 event.preventDefault();
10
11 const amountToCharge = 100;
12
13 const cardElement = elements?.getElement(CardElement);
14
15 if (!stripe || !elements || !cardElement) {
16 return;
17 }
18
19 const stripeResponse = await stripe.createPaymentMethod({
20 type: 'card',
21 card: cardElement
22 });
23
24 const { error, paymentMethod } = stripeResponse;
25
26 if (error || !paymentMethod) {
27 return;
28 }
29
30 const paymentMethodId = paymentMethod.id;
31
32 fetch(`${process.env.REACT_APP_API_URL}/charge`, {
33 method: 'POST',
34 body: JSON.stringify(({
35 paymentMethodId,
36 amount: amountToCharge
37 })),
38 credentials: 'include',
39 headers: {
40 'Content-Type': 'application/json'
41 },
42 })
43
44 };
45
46 return {
47 handleSubmit
48 }
49}
50
51export default usePaymentForm;The 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:
1stripe.createPaymentMethod({
2 type: 'card',
3 card: cardElement
4});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 100 as 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!