Better Payment

Express, Fastify, Hono

Mount the payment handler in Express, Fastify, Hono 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 OK as plain text
  • keep the Location header 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.

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));

On this page