DEV Community

Ian de Jesus
Ian de Jesus

Posted on • Edited on

12 1 1

PayPal Integration Using NextJS and Prisma

Github Repository

Project Demo

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.

  1. 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
  2. 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
Enter fullscreen mode Exit fullscreen mode

Install all required libraries

pnpm i @paypal/checkout-server-sdk prisma @prisma/client prisma @paypal/react-paypal-js axios react-query
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Setup Paypal

Create an .env file and add your PayPal Client and Secret

NEXT_PUBLIC_PAYPAL_CLIENT_ID=
PAYPAL_CLIENT_SECRET=
PAYPAL_CLIENT_ID=
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 })
}
Enter fullscreen mode Exit fullscreen mode

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 })
}
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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.

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read full post →

Top comments (0)

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more

👋 Kindness is contagious

Explore a sea of insights with this enlightening post, highly esteemed within the nurturing DEV Community. Coders of all stripes are invited to participate and contribute to our shared knowledge.

Expressing gratitude with a simple "thank you" can make a big impact. Leave your thanks in the comments!

On DEV, exchanging ideas smooths our way and strengthens our community bonds. Found this useful? A quick note of thanks to the author can mean a lot.

Okay