zy1p's blog

How to Migrate from PayPal REST API Calls to the Official Server SDK in Next.js

3 min read

Learn why and how I migrated from manual PayPal REST API calls to the official Server SDK in a Next.js + TypeScript project, gaining type safety and more stable payments.

Contents

Motivations

I had a PayPal integration running smoothly for over a year (using REST API). A month ago, customers suddenly reported payment failures. Logs showed that calls to PayPal REST endpoints were being blocked by Cloudflare. I didn’t have a clear root cause yet, so instead of debugging at the edge, I tried the newly stable @paypal/paypal-server-sdk to simplify my stack and reduce moving parts.

Stack context: Next.js + TanStack Query + tRPC + Drizzle ORM.

Migration to PayPal Server SDK

Switching was surprisingly straightforward:

  1. Initialize client with existing PayPal Client ID and Secret.
  2. Use the Order Controller from the SDK.
  3. Replace my manual axios calls with:
    • createOrder
    • captureOrder

Previous Approach (PayPal REST API)

import type { AxiosError } from 'axios'
import {
type CreateOrderRequestBody,
type OrderResponseBody,
} from '@paypal/paypal-js'
import axios from 'axios'
import { z } from 'zod'
import { env } from '~/env'
export const paypalClient = axios.create({
baseURL: env.PAYPAL_API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
})
paypalClient.interceptors.request.use(
async function (config) {
switch (config.url) {
case '/v1/oauth2/token':
return config
default:
const accessToken = await generateAccessToken()
config.headers.setAuthorization(`Bearer ${accessToken}`)
return config
}
},
function (error) {
return Promise.reject(error)
}
)
paypalClient.interceptors.response.use(
function (response) {
return response
},
function (error) {
console.log(error)
return Promise.reject(error)
}
)
async function generateAccessToken() {
const { data } = await paypalClient.post<{ access_token: string }>(
'/v1/oauth2/token',
'grant_type=client_credentials',
{
auth: {
username: env.NEXT_PUBLIC_PAYPAL_CLIENT_ID,
password: env.PAYPAL_CLIENT_SECRET,
},
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}
)
return data.access_token
}
export async function createOrder(data: CreateOrderRequestBody) {
const res = await paypalClient.post<OrderResponseBody>(
'/v2/checkout/orders',
data
)
return res.data
}
export async function capturePayment(orderID: string) {
const res = await paypalClient.post<OrderResponseBody>(
`/v2/checkout/orders/${orderID}/capture`
)
return res.data
}

New Approach (PayPal Server SDK)

import {
Client,
Environment,
LogLevel,
OrdersController,
} from '@paypal/paypal-server-sdk'
import { env } from '~/env'
const client = new Client({
clientCredentialsAuthCredentials: {
oAuthClientId: env.NEXT_PUBLIC_PAYPAL_CLIENT_ID,
oAuthClientSecret: env.PAYPAL_CLIENT_SECRET,
},
timeout: 0,
environment:
env.NODE_ENV === 'production'
? Environment.Production
: Environment.Sandbox,
logging: {
logLevel: LogLevel.Info,
logRequest: {
logBody: true,
},
logResponse: {
logHeaders: true,
},
},
})
export const orderController = new OrdersController(client)
// In a tRPC create order route:
const res = await orderController.createOrder({ body })
// In a tRPC capture order route:
const res = await orderController.captureOrder({
id: input.paypalOrderID,
})

Results

Under the hood, the SDK still talks to the same REST API endpoints, but now it also handles DTO parsing, type safety, and error formatting.

Limitation

The PayPal Server SDK provides integration access to the PayPal REST APIs. The API endpoints are divided into distinct controllers:

For me, this means my main payment flows (creating and capturing orders) are fully supported, which is enough for most of my day-to-day needs. But if I ever need other PayPal features outside these controllers, I’ll still fall back to my old REST API approach, which I already have working.

Conclusion

Since my workflow is mostly about creating and capturing orders, the PayPal Server SDK fits perfectly. It cuts down boilerplate, adds type safety, and handles the low-level REST details for me. For anything else, I can just keep using my existing REST integration. This way, I get the best of both worlds — cleaner code for my main payment logic, and flexibility for the rest.

References