Tutorials
Build a Node.js API Proxy
Put Honeycluster traffic behind your own auth and rate-limits with a small Express forwarder — useful for enterprise keys or for throttling your own users against the shared-tier fair-use ceiling.

The public honeycluster.io endpoint is keyless, so a proxy isn't required for basic access — browsers can talk to it directly. But you'll still want a server-side proxy when:

  • •
    You're on a private / enterprise plan with an API key. Keys grant project-wide access and must never ship to the browser.
  • •
    You want to authorize your own users before they consume Honeycluster capacity (session cookie, JWT, internal bearer token) so random traffic can't burn your shared-tier budget.
  • •
    You want to pre-filter or rewrite requests — e.g. force a subset of allowed method values, inject default params, or add caching that's shared across all your users.

This tutorial builds that proxy with Express and xrpl.js.

1. Dependencies
##
Bash
pnpm add express xrpl
pnpm add -D @types/express typescript tsx
2. The server
##
TypeScript
// server.ts
import express from 'express'
import { Client } from 'xrpl'

const app = express()
const PORT = Number(process.env.PORT ?? 4000)

// Public cluster — no key. For a private endpoint, add:
//   { headers: { 'X-API-Key': process.env.HONEYCLUSTER_API_KEY! } }
const client = new Client('wss://9f21fd99-7552e74b.sandblocks.dev/proxy/xrpl-ws')

await client.connect()

app.get('/api/ledger/:index', async (req, res) => {
  const ledger_index = Number(req.params.index)
  if (!Number.isFinite(ledger_index)) {
    return res.status(400).json({ error: 'ledger_index must be numeric' })
  }

  try {
    const { result } = await client.request({
      command: 'ledger',
      ledger_index,
      transactions: true,
      expand: true,
    })
    res.json(result.ledger)
  } catch (err) {
    res.status(502).json({ error: (err as Error).message })
  }
})

app.listen(PORT, () => console.log(`proxy listening on :${PORT}`))

Two things to notice:

  1. •
    The xrpl client is opened once, at startup, and reused for every request. Creating a new WebSocket per call would be slow and chew through connection budget.
  2. •
    Any API key lives only in process.env. It never travels to the caller, never appears in response headers, and never lands in client-side code.
3. Authenticate your callers
##

If you're exposing this proxy to an untrusted client (like a browser), add your own authorization middleware before the proxy handlers — session cookies, a JWT from your auth service, or a short-lived per-user token:

TypeScript
app.use('/api', (req, res, next) => {
  const token = req.header('authorization')?.replace(/^Bearer /, '')
  if (!token || !verifyUserToken(token)) {
    return res.status(401).json({ error: 'unauthorized' })
  }
  next()
})

Without this, anyone on the internet can hit your proxy and consume capacity under your IP (or, for private plans, spend your credits).

4. Cache what you can
##

Historical ledger data is immutable — the same ledger index always returns the same payload. Cache aggressively to cut upstream traffic (and, on private plans, credit spend):

TypeScript
import { LRUCache } from 'lru-cache'

const ledgerCache = new LRUCache<number, unknown>({ max: 500 })

app.get('/api/ledger/:index', async (req, res) => {
  const ledger_index = Number(req.params.index)
  const cached = ledgerCache.get(ledger_index)
  if (cached) return res.json(cached)

  const { result } = await client.request({
    command: 'ledger',
    ledger_index,
    transactions: true,
    expand: true,
  })
  ledgerCache.set(ledger_index, result.ledger)
  res.json(result.ledger)
})

For larger deployments, swap the in-memory LRU for Redis and share the cache across pods.

5. Forward errors faithfully
##

Honeycluster's errors are already structured. Don't mask them — proxy them through so clients see the real cause:

TypeScript
try {
  // ...call client.request...
} catch (err: any) {
  const status = err?.data?.error === 'notFound' ? 404 : 502
  res.status(status).json({
    code: err?.data?.error ?? 'UPSTREAM_ERROR',
    message: err?.message ?? 'Upstream request failed',
  })
}

See the Error Codes reference for the full list of codes your proxy might relay.