Home / Uncategorized / Implementasi Backend Minimal Menggunakan Node.js + Express untuk Midtrans dan Xendit

Implementasi Backend Minimal Menggunakan Node.js + Express untuk Midtrans dan Xendit

Implementasi Backend Minimal Menggunakan Node.js + Express untuk Midtrans dan Xendit
Bot image for Assistant

Shared conversation

Assistant ·

Implementasi Backend Minimal Menggunakan Node.js + Express untuk Midtrans dan Xendit

1. Struktur Proyek

project-root/
  ├── server.js            (Express server)
  ├── package.json
  └── README.md

2. Environment Variables

Untuk Midtrans:

  • MIDTRANS_SERVER_KEY=your_midtrans_server_key
  • MIDTRANS_CLIENT_KEY=your_midtrans_client_key
  • NODE_ENV=development

Untuk Xendit:

  • XENDIT_SECRET_API_KEY=your_xendit_secret_api_key

Porta server default: 3000

3. Kode Server (server.js)

javascript
const express = require('express');
const fetch = require('node-fetch');
const bodyParser = require('body-parser');
const crypto = require('crypto');
const app = express();

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

const ORDERS = {};

function genOrderId() {
  return 'ORD-' + Date.now();
}

// MIDTRANS (Snap) - Server
const MIDTRANS_SERVER_KEY = process.env.MIDTRANS_SERVER_KEY || '';
const MIDTRANS_CLIENT_KEY = process.env.MIDTRANS_CLIENT_KEY || ''; 
const MIDTRANS_BASE = 'https://api.sandbox.midtrans.com/v2';

function midtransAuthHeader() {
  return 'Basic ' + Buffer.from(MIDTRANS_SERVER_KEY + ':').toString('base64');
}

app.post('/midtrans/create-transaction', async (req, res) => {
  try {
    const { name, email, ticketType, price } = req.body;
    if (!name || !email || !ticketType || !price) {
      return res.status(400).json({ error: 'Missing fields' });
    }

    const orderId = genOrderId();
    ORDERS[orderId] = {
      orderId, name, email, ticketType, price, status: 'pending'
    };

    const payload = {
      transaction_details: {
        order_id: orderId,
        gross_amount: Number(price)
      },
      credit_card: {
        secure: true
      },
      customer_details: {
        first_name: name,
        email: email
      }
    };

    const resp = await fetch(`${MIDTRANS_BASE}/transactions`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': midtransAuthHeader()
      },
      body: JSON.stringify(payload)
    });

    if (!resp.ok) {
      const text = await resp.text();
      return res.status(502).json({ error: 'Midtrans API error', detail: text });
    }

    const data = await resp.json();
    ORDERS[orderId].midtrans = data;

    return res.json({ orderId, midtrans: data, midtransClientKey: MIDTRANS_CLIENT_KEY });
  } catch (err) {
    res.status(500).json({ error: 'server_error' });
  }
});

// Midtrans webhook
app.post('/midtrans/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  try {
    const rawBody = req.body.toString('utf8');
    const payload = JSON.parse(rawBody);
    const { order_id, status_code, gross_amount, signature_key } = payload;
    const expected = crypto.createHash('sha512').update(order_id + status_code + gross_amount + MIDTRANS_SERVER_KEY).digest('hex');

    if (expected !== signature_key) {
      return res.status(400).send('invalid signature');
    }

    const order = ORDERS[order_id];
    if (!order) {
      return res.status(200).send('order not found');
    }

    if (payload.transaction_status === 'settlement') {
      order.status = 'paid';
    } else {
      order.status = 'failed';
    }

    res.status(200).send('ok');
  } catch (err) {
    res.status(500).send('server error');
  }
});

// XENDIT - Server
const XENDIT_SECRET_API_KEY = process.env.XENDIT_SECRET_API_KEY || '';
const XENDIT_BASE = 'https://api.xendit.co';

app.post('/xendit/create-invoice', async (req, res) => {
  try {
    const { name, email, ticketType, price } = req.body;
    if (!name || !email || !ticketType || !price) {
      return res.status(400).json({ error: 'Missing fields' });
    }
    const orderId = genOrderId();
    ORDERS[orderId] = { orderId, name, email, ticketType, price, status: 'pending' };

    const body = {
      external_id: orderId,
      amount: Number(price),
      payer_email: email,
      description: `Ticket ${ticketType} - ${orderId}`
    };

    const resp = await fetch(`${XENDIT_BASE}/v2/invoices`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Basic ' + Buffer.from(XENDIT_SECRET_API_KEY + ':').toString('base64')
      },
      body: JSON.stringify(body)
    });

    if (!resp.ok) {
      const t = await resp.text();
      return res.status(502).json({ error: 'Xendit API error', detail: t });
    }

    const data = await resp.json();
    ORDERS[orderId].xendit = data;
    return res.json({ orderId, invoice: data });
  } catch (err) {
    res.status(500).json({ error: 'server_error' });
  }
});

// Xendit webhook
app.post('/xendit/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  try {
    const signature = req.headers['x-callback-signature'] || '';
    const payload = req.body.toString('utf8');
    const expected = crypto.createHmac('sha256', XENDIT_SECRET_API_KEY).update(payload).digest('hex');

    if (signature !== expected) {
      return res.status(400).send('invalid signature');
    }

    const data = JSON.parse(payload);
    if (data.type === 'invoice' && data.data && data.data.status) {
      const external_id = data.data.external_id;
      const order = ORDERS[external_id];
      if (order && data.data.status === 'PAID') {
        order.status = 'paid';
      }
    }

    res.status(200).send('ok');
  } catch (err) {
    res.status(500).send('server error');
  }
});

// Utility endpoints
app.get('/order/:id', (req, res) => {
  const order = ORDERS[req.params.id];
  if (!order) return res.status(404).json({ error: 'not found' });
  res.json(order);
});

app.get('/', (req, res) => res.send('Payment demo server running'));

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log('Server listening on', PORT));

4. package.json

json
{
  "name": "payment-demo",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "body-parser": "^1.20.0",
    "express": "^4.18.2",
    "node-fetch": "^2.6.7"
  }
}

5. Contoh Panggilan dari Front-End

javascript
async function checkoutWithMidtrans(name, email, ticketType, price) {
  const resp = await fetch('/midtrans/create-transaction', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name, email, ticketType, price })
  });
  const data = await resp.json();
  if (data.midtrans && data.midtrans.redirect_url) {
    window.location = data.midtrans.redirect_url;
  }
}

async function checkoutWithXendit(name, email, ticketType, price) {
  const resp = await fetch('/xendit/create-invoice', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name, email, ticketType, price })
  });
  const data = await resp.json();
  if (data.invoice && data.invoice.invoice_url) {
    window.location = data.invoice.invoice_url;
  }
}

6. Testing Lokal dengan Ngrok

  1. Jalankan server: npm install && npm start
  2. Jalankan ngrok: ngrok http 3000
  3. Gunakan URL ngrok sebagai callback/webhook URL di dashboard Midtrans/Xendit.

7. Generasi Tiket & QR Code (Opsional)

  • Setelah status order menjadi paid, Anda bisa menggunakan library untuk menghasilkan QR code atau PDF.

8. Keamanan & Produksi

  • Jangan simpan API keys di repo.
  • Gunakan HTTPS.
  • Validasi semua input dan simpan transaksi di database.

9. Permintaan Tambahan

Beritahu jika Anda ingin menambahkan contoh implementasi tertentu atau jika ada bagian lain yang ingin Anda kembangkan lebih lanjut.

Bot image for Assistant
Go to @Assistant on Poe
Tagged: