Express, Fastify, Hono
Mount the payment handler in Express, Fastify, Hono, Elysia, React Router or a plain Node.js server.
Each adapter is a separate import and has no dependencies, so you only load the one you use. For Next.js, see the Next.js guide; for Workers, Deno and Bun, see edge runtimes.
All adapters:
- pass JSON and form-urlencoded bodies (the bank callbacks) to the handler
- send PayTR's
OKas plain text - keep the
Locationheader of redirects (callbackRedirect)
Register them under the handler's basePath (default /api/pay).
Express
import express from 'express';
import { toExpressHandler } from 'better-payment/express';
import { payment } from './payment';
const app = express();
app.all('/api/pay/*path', toExpressHandler(payment)); // Express 4: '/api/pay/*'You don't need express.json() or express.urlencoded() for the payment routes, because the adapter reads the raw body. If a body parser runs first, its result is used. Unexpected errors are passed to next(error).
Fastify
import Fastify from 'fastify';
import { toFastifyPlugin } from 'better-payment/fastify';
const app = Fastify();
await app.register(toFastifyPlugin(payment), { prefix: '/api/pay' });Fastify rejects application/x-www-form-urlencoded bodies by default (415), but iyzico, Param, Akbank and PayTR post their callbacks in that format. The plugin accepts it for its own routes only, so your other routes keep Fastify's defaults.
Hono
import { Hono } from 'hono';
import { toHonoHandler } from 'better-payment/hono';
const app = new Hono();
app.all('/api/pay/*', toHonoHandler(payment));Hono runs on Node.js, Bun, Deno and Cloudflare Workers, like Better Payment.
Elysia
import { Elysia } from 'elysia';
import { toElysiaHandler } from 'better-payment/elysia';
const app = new Elysia();
app.all('/api/pay/*', toElysiaHandler(payment), { parse: 'none' });{ parse: 'none' } keeps the raw body for the handler: bank callbacks are form-urlencoded and their signatures are checked against the original fields. Without it, any hook that reads body (onTransform, derive, onBeforeHandle or a plugin) makes Elysia consume the request first. Elysia is built for Bun and also runs on Node.js.
React Router
Added in 0.8.0// app/routes/api.pay.$.ts
import { toReactRouterHandler } from 'better-payment/react-router';
import { getBetterPayment } from '~/lib/payment.server';
export const { loader, action } = toReactRouterHandler(getBetterPayment);A splat resource route (api.pay.$.ts matches everything under /api/pay) with no default export: GET requests go to loader, POST requests (bank callbacks included) to action. The same code works in Remix v2. With routes.ts config, register it as route('api/pay/*', 'routes/api.pay.$.ts').
Plain Node.js
import { createServer } from 'node:http';
import { toNodeHandler } from 'better-payment/express';
createServer(toNodeHandler(payment)).listen(3000);Lazy initialization
Every adapter also accepts a function that returns the payment object (or its handler). The function is called on each request, so you can create the instance on first use:
const createPayment = () => betterPayment({ /* ... */ });
let instance: ReturnType<typeof createPayment> | undefined;
const getPayment = () => (instance ??= createPayment());
app.all('/api/pay/*path', toExpressHandler(getPayment));Request body size
Added in 0.8.1The Express, Node.js and fetch-based adapters read at most 1 MiB of a request body and answer 413 for larger ones. Bank callbacks are a few kB. Change the limit with maxBodySize (in bytes) on toExpressHandler, toNodeHandler or toFetchHandler:
app.all('/api/pay/*path', toExpressHandler(payment, { maxBodySize: 64 * 1024 }));Fastify applies its own bodyLimit (1 MiB by default).