Make a Moonbeam RPC Call in Node.js, Then Know When You Need an Indexer
A hands-on guide to calling a Moonbeam RPC endpoint from Node.js 18+, reading the result correctly, handling failures, and recognizing the point where RPC calls stop being the right tool and indexing takes over.
This guide answers one developer task: send a real JSON-RPC request to a Moonbeam node from Node.js 18+, interpret the response, and handle the common failure modes. It then draws a clear line between what an RPC endpoint can tell you and what an indexing service is for, so you do not try to build a history query out of repeated RPC calls. You will need a Moonbeam RPC endpoint URL from a provider or your own node; no endpoint is hardcoded here because you should use the one you actually control.
Step 1: Get an endpoint you control
Before writing code, decide where your RPC traffic goes. You can run your own Moonbeam node, or use a managed RPC provider. If you are evaluating managed access, the OnFinality Moonbeam network page lists the network and its RPC options, and the RPC Assistant helps you assemble a request against a provider endpoint. Treat any endpoint URL as a secret-ish configuration value: put it in an environment variable, not in source control.
Moonbeam is EVM-compatible, so standard Ethereum JSON-RPC methods such as eth_blockNumber, eth_getBalance, and eth_call work against it. That compatibility is why the same client code you use for Ethereum usually works here with only the URL changed.
- Store the endpoint in an environment variable such as MOONBEAM_RPC_URL.
- Never commit a provider URL that contains an API key.
- Confirm the chain ID with eth_chainId before trusting any other response.
Step 2: A runnable Node.js 18+ example
Node.js 18 ships a global fetch, so you do not need a dependency. The script below sends eth_chainId and eth_blockNumber, checks the HTTP status, checks for a JSON-RPC error object, and validates that the result is what you expect. Run it with MOONBEAM_RPC_URL=... node moonbeam-check.mjs.
// moonbeam-check.mjs — Node.js 18+
const url = process.env.MOONBEAM_RPC_URL;
if (!url) {
console.error('Missing MOONBEAM_RPC_URL environment variable.');
process.exit(1);
}
async function rpc(method, params = []) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10_000);
try {
const res = await fetch(url, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
signal: controller.signal,
});
if (!res.ok) {
throw new Error(`HTTP ${res.status} ${res.statusText}`);
}
const body = await res.json();
if (body.error) {
throw new Error(`RPC error ${body.error.code}: ${body.error.message}`);
}
if (body.result === undefined) {
throw new Error('Malformed response: no result field');
}
return body.result;
} finally {
clearTimeout(timeout);
}
}
try {
const chainId = await rpc('eth_chainId');
const blockNumber = await rpc('eth_blockNumber');
console.log('chainId (hex):', chainId);
console.log('chainId (decimal):', parseInt(chainId, 16));
console.log('latest block (hex):', blockNumber);
console.log('latest block (decimal):', parseInt(blockNumber, 16));
} catch (err) {
console.error('RPC call failed:', err.message);
process.exit(1);
}
Expected result: two hex strings printed, plus their decimal equivalents. The chain ID should match the Moonbeam network you intended to reach; if it does not, you are pointed at the wrong chain and every later result is suspect.
If you prefer curl, the same request is: curl -s -X POST -H 'content-type: application/json' --data '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' "$MOONBEAM_RPC_URL". A healthy response is a JSON object with a result field containing a hex block number.
Step 3: Handle the failures you will actually see
RPC failures are rarely mysterious once you separate transport problems from protocol problems. A non-2xx HTTP status means the request never reached a working JSON-RPC handler — bad URL, auth failure, or rate limiting. A 200 response with an error object means the node understood you and refused: unknown method, invalid params, or a range it will not serve. A timeout means the node did not answer in time, which is different from an error and should be retried with backoff rather than treated as a permanent failure.
Do not silently swallow these. Log the method, the HTTP status, and the error code so you can tell them apart later. If you are seeing repeated rate-limit responses, that is a signal about your request volume, not about the chain.
- Distinguish HTTP errors (transport/auth) from JSON-RPC errors (protocol).
- Use AbortController so a hung request cannot stall your process.
- Retry timeouts and 5xx with exponential backoff; do not retry a 4xx blindly.
- Log the method name alongside the error so debugging is possible.
Step 4: Know when RPC stops being the right tool
An RPC endpoint answers questions about current or specific state: the latest block, a balance at a block, the result of a read-only contract call, a transaction by hash. It is not a query engine. If you need "every transfer to this contract in the last 30 days" or "all accounts that interacted with this address," you would have to scan blocks one at a time over RPC, which is slow, fragile, and expensive in request volume. That is the boundary where indexing belongs.
An indexer ingests blocks and stores derived data in a queryable form, so you ask a structured question and get a filtered result set. The distinction is about the shape of the question, not about which service is "better." Use RPC for point lookups and writes; use indexing for history, aggregation, and filtered lists. If you are unsure which side of the line your task falls on, the OnFinality Learn library covers the concepts behind both approaches.
- RPC: latest state, single-block reads, contract calls, transaction lookup.
- Indexing: historical ranges, aggregations, filtered lists, cross-block joins.
- Scanning many blocks over RPC to build history is a sign you need an indexer.
- Measure your own request volume and latency before assuming either approach will scale for you.
Next step
Take the script above and point it at the Moonbeam endpoint you intend to use, then verify the chain ID matches. Once that works, try eth_getBalance for an address you know, and note how the request shape stays the same while the params change. When you hit a question that needs history rather than a point lookup, that is your cue to move to an indexing workflow rather than looping RPC calls.
If you want help constructing the next request or checking a method signature, the OnFinality RPC Assistant is a practical place to start.
- Verify chain ID first, every time you change endpoints.
- Keep RPC for point reads; reach for indexing when the question is historical or aggregated.
- Measure latency and error rates yourself rather than relying on any provider's marketing numbers.