Next.js
Integrating Better Payment with the Next.js App Router.
1. Create the singleton
// lib/payment.ts
import { betterPayment, iyzico } from 'better-payment';
const createPayment = () =>
betterPayment({
mode: process.env.NODE_ENV === 'production' ? 'production' : 'sandbox',
providers: {
iyzico: iyzico({
apiKey: process.env.IYZICO_API_KEY!,
secretKey: process.env.IYZICO_SECRET_KEY!,
}),
},
handler: {
allowedActions: ['payment/init-3ds', 'payment/complete-3ds', 'callback'],
transformRequest: async (ctx) => buildPaymentRequestFromOrder(ctx.body?.orderId, ctx.body?.paymentCard),
onCallback: async (result) => markOrder(result.paymentId, result.status),
callbackRedirect: (result) => `/orders/${result.paymentId}?payment=${result.status}`,
},
});
let instance: ReturnType<typeof createPayment> | undefined;
export const getBetterPayment = () => (instance ??= createPayment());Initialize lazily (in a function, not with a module-level betterPayment(...) call). Otherwise Next.js static generation can crash when the environment variables are not available.
2. Create the catch-all route handler
// app/api/pay/[...path]/route.ts
import { toNextJsHandler } from 'better-payment/next';
import { getBetterPayment } from '@/lib/payment';
export const { GET, POST } = toNextJsHandler(getBetterPayment);Passing the getBetterPayment function (not its result) keeps the initialization lazy: the instance is created on the first request, so next build works without the environment variables.
The adapter passes JSON and form-urlencoded bodies (the iyzico, Param, Akbank and PayTR callbacks) to the handler as raw text, and writes the handler's response unchanged:
- PayTR's plain-text
OKis not JSON-encoded. - Redirects (
callbackRedirect) keep theirLocationheader and have no body.
The route works on both runtimes. Add export const runtime = 'edge' to run it on the edge runtime.
3. Start a payment from the frontend
const res = await fetch('/api/pay/iyzico/payment/init-3ds', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ orderId, paymentCard }), // amounts come from transformRequest
});
const result = await res.json();
if (result.status === 'pending' && result.threeDSHtmlContent) {
document.open();
document.write(result.threeDSHtmlContent);
document.close();
}Or use the typed client:
import { createBetterPaymentClient } from 'better-payment/client';
const client = createBetterPaymentClient({ baseUrl: '/api/pay' });
const result = await client.iyzico.initThreeDSPayment(requestBuiltByTransformRequest);
// 422 (provider failure) resolves with status 'failure' instead of throwing4. Callback
Set callbackUrl to https://yoursite.com/api/pay/<provider>/payment/complete-3ds.
The handler verifies the callback, calls onCallback, and redirects the customer
using callbackRedirect.
For PayTR, set the notification URL in the PayTR panel to
https://yoursite.com/api/pay/paytr/callback.
Complete example
examples/next-shop is a runnable App Router shop built with this
setup: Prisma orders, amounts from transformRequest, a verified 3D Secure callback, payment events
and a privileged refund route. It runs on MockProvider without credentials.