PayPal Integration Using NextJS and Prisma

This tutorial explains how to use NextJS as the backend to create and capture PayPal orders and store the order data in SQLite using Prisma.
  • When the user clicks the PayPal button, we make a post request to /paypal/createOrder to create a new Paypal Order. The OrderID and a status of PENDING is stored in SQLite through Prisma
  • After the user pays for the order, we make a post request to paypal\captureOrder to capture the payment and then update the status to PAID.
  • Initial Setup
    pnpx create-next-app --typescript
    Install all required libraries
    pnpm i @paypal/checkout-server-sdk prisma @prisma/client prisma @paypal/react-paypal-js axios react-query
    Add baseUrl: "./" in tsconfig.json to make imports easy to read.
    Setup Prisma
    Create a prisma/schema.prisma
    generator client {
            provider = "prisma-client-js"
    }
    
    datasource db {
            provider = "sqlite"
            url      = "file:./dev.db"
    }
    
    
    model Payment {
            id      Int    @id @default(autoincrement())
            orderID String
            status  String
    }
    To keep it simple we will only be storing the PayPal OrderID and its status
    Lets migrate and generate our Prisma client
    pnpm prisma migrate dev
    pnpm prisma generate
    Create lib/prisma.ts
    import {PrismaClient} from '@prisma/client'
    
    // Prevent multiple instances of Prisma Client in development
    declare const global: typeof globalThis & {prisma?: PrismaClient}
    
    const prisma = global.prisma || new PrismaClient()
    if (process.env.NODE_ENV === 'development') global.prisma = prisma
    
    export default prisma
    Setup Paypal
    Create an .env file and add your PayPal Client and Secret
    NEXT_PUBLIC_PAYPAL_CLIENT_ID=
    PAYPAL_CLIENT_SECRET=
    PAYPAL_CLIENT_ID=
    Create lib/paypal.ts
    import checkoutNodeJssdk from '@paypal/checkout-server-sdk'
    
    const configureEnvironment = function () {
      const clientId = process.env.PAYPAL_CLIENT_ID
      const clientSecret = process.env.PAYPAL_CLIENT_SECRET
    
      return process.env.NODE_ENV === 'production'
        ? new checkoutNodeJssdk.core.LiveEnvironment(clientId, clientSecret)
        : new checkoutNodeJssdk.core.SandboxEnvironment(clientId, clientSecret)
    }
    
    const client = function () {
      return new checkoutNodeJssdk.core.PayPalHttpClient(configureEnvironment())
    }
    
    export default client
    We will use the client to create and capture orders.
    Let us now create our 2 API endpoints.
    Create /api/paypal/createOrder.ts
    import prisma from 'lib/prisma'
    import type { NextApiRequest, NextApiResponse } from 'next'
    import client from 'lib/paypal'
    import paypal from '@paypal/checkout-server-sdk'
    
    export default async function handle(
      req: NextApiRequest,
      res: NextApiResponse,
    ) {
      const PaypalClient = client()
      //This code is lifted from https://github.com/paypal/Checkout-NodeJS-SDK
      const request = new paypal.orders.OrdersCreateRequest()
      request.headers['prefer'] = 'return=representation'
      request.requestBody({
        intent: 'CAPTURE',
        purchase_units: [
          {
            amount: {
              currency_code: 'PHP',
              value: '100.00',
            },
          },
        ],
      })
      const response = await PaypalClient.execute(request)
      if (response.statusCode !== 201) {
        res.status(500)
      }
    
      //Once order is created store the data using Prisma
      await prisma.payment.create({
        data: {
          orderID: response.result.id,
          status: 'PENDING',
        },
      })
      res.json({ orderID: response.result.id })
    }
    Create api/paypal/captureOrder.ts
    import type { NextApiRequest, NextApiResponse } from 'next'
    import client from 'lib/paypal'
    import paypal from '@paypal/checkout-server-sdk'
    import prisma from 'lib/prisma'
    
    export default async function handle(
      req: NextApiRequest,
      res: NextApiResponse,
    ) {
      //Capture order to complete payment
      const { orderID } = req.body
      const PaypalClient = client()
      const request = new paypal.orders.OrdersCaptureRequest(orderID)
      request.requestBody({})
      const response = await PaypalClient.execute(request)
      if (!response) {
        res.status(500)
      }
    
      // Update payment to PAID status once completed
      await prisma.payment.updateMany({
        where: {
          orderID,
        },
        data: {
          status: 'PAID',
        },
      })
      res.json({ ...response.result })
    }
    Setup FrontEnd
    Update _app.tsx
    import '../styles/globals.css'
    import type {AppProps} from 'next/app'
    import {QueryClient, QueryClientProvider} from 'react-query'
    
    const queryClient = new QueryClient()
    
    function MyApp({Component, pageProps}: AppProps) {
      return (
        <QueryClientProvider client={queryClient}>
          <Component {...pageProps} />
        </QueryClientProvider>
      )
    }
    export default MyApp
    We use react-query for ease of making async calls
    In index.tsx
    import axios, { AxiosError } from 'axios'
    import Head from 'next/head'
    import Image from 'next/image'
    import { useMutation } from 'react-query'
    import styles from '../styles/Home.module.css'
    import {
      PayPalScriptProvider,
      PayPalButtons,
      FUNDING,
    } from '@paypal/react-paypal-js'
    
    export default function Home() {
      const createMutation = useMutation<{ data: any }, AxiosError, any, Response>(
        (): any => axios.post('/api/paypal/createOrder'),
      )
      const captureMutation = useMutation<string, AxiosError, any, Response>(
        (data): any => axios.post('/api/paypal/captureOrder', data),
      )
      const createPayPalOrder = async (): Promise<string> => {
        const response = await createMutation.mutateAsync({})
        return response.data.orderID
      }
    
      const onApprove = async (data: OnApproveData): Promise<void> => {
        return captureMutation.mutate({ orderID: data.orderID })
      }
      return (
        <div className={styles.container}>
          <Head>
            <title>Create Next App</title>
            <meta name="description" content="Generated by create next app" />
            <link rel="icon" href="/favicon.ico" />
          </Head>
          <main className={styles.main}>
            {captureMutation.data && (
              <div>{JSON.stringify(captureMutation.data)}</div>
            )}
            <PayPalScriptProvider
              options={{
                'client-id': process.env.NEXT_PUBLIC_PAYPAL_CLIENT_ID as string,
                currency: 'PHP',
              }}
            >
              <PayPalButtons
                style={{
                  color: 'gold',
                  shape: 'rect',
                  label: 'pay',
                  height: 50,
                }}
                fundingSource={FUNDING.PAYPAL}
                createOrder={createPayPalOrder}
                onApprove={onApprove}
              />
            </PayPalScriptProvider>
          </main>
    
          <footer className={styles.footer}>
            <a
              href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
              target="_blank"
              rel="noopener noreferrer"
            >
              Powered by{' '}
              <span className={styles.logo}>
                <Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
              </span>
            </a>
          </footer>
        </div>
      )
    }
    
    declare global {
      interface Window {
        paypal: any
      }
    }
    
    interface OnApproveData {
      billingToken?: string | null
      facilitatorAccessToken: string
      orderID: string
      payerID?: string | null
      paymentID?: string | null
      subscriptionID?: string | null
      authCode?: string | null
    }
    We declare createPaypalOrder and onApprove functions. Both functions are passed as props to the PayPal Button. createPaypalOrder is initially called on click of the PayPal button which triggers creation of the PayPal Order. The mutation returns the orderID that will be consumed by the onApprove function to capture the payment. After successful payment, the result is JSON stringified and displayed in the browser.

    30

    This website collects cookies to deliver better user experience

    PayPal Integration Using NextJS and Prisma