Blockchain API integration: a practical guide for Web3 devs
A failed blockchain API integration rarely fails at the obvious boundary. The wallet connects. The contract call returns. The transaction hash appears.

That is the attack surface. Not only theft. Incorrect state. Incorrect assumptions. Unbounded calls. Hidden trust in infrastructure that was never part of the smart contract threat model.
A blockchain API is not one thing. It is a stack of interfaces: RPC providers, client libraries, wallet connectors, indexing protocols, and oracle networks. Each layer mutates what the application believes about chain state. Each layer can fail deterministically, intermittently, or adversarially. Production Web3 API integration starts by separating these layers and assigning each one a narrow job.
The integration boundary: what the frontend is allowed to know
The first invariant is simple.
The frontend does not own truth. It observes truth.
The EVM state root is truth. A successful transaction receipt is evidence. An RPC response is a report. A subgraph response is an indexed projection. A wallet signature is user intent under a specific chain context. These are not interchangeable.
Most broken integrations collapse them into one object called “data.”
A practical blockchain API stack usually contains five distinct paths:
1. Direct contract reads through an Ethereum API node connection, using JSON-RPC methods such as eth_call.
2. Transaction construction and signing through a wallet, usually mediated by a library.
3. Event and historical data retrieval through an indexing layer such as The Graph.
4. Off-chain data injection into contracts through oracle networks such as Chainlink Data Feeds.
5. Application state synchronization through React hooks or another state layer.
Each path has a different latency model and failure mode. Direct RPC reads are current but expensive at scale. Indexed queries are cheap and expressive but may lag. Wallet signing is user-gated and chain-sensitive. Oracle data is not a frontend feed; it is a contract-readable input with its own update cadence and aggregation model.
This is where the stack choice matters. Not for taste. For control.
| Layer | Common tool | What it should do | What it should not do |
|---|---|---|---|
| EVM client primitives | viem or ethers.js v6 | Encode calls, read contracts, send transactions, parse logs | Hide chain identity or normalize unsafe assumptions |
| React state | wagmi | Manage wallet state, contract reads, transaction hooks | Become the source of canonical truth |
| Historical querying | The Graph | Query indexed events and derived entities with GraphQL | Serve as a real-time database |
| Oracle input | Chainlink Data Feeds | Provide off-chain data to contracts through decentralized oracle networks | Feed frontend-only pricing into settlement logic |
| RPC provider | Alchemy, Infura, self-hosted node, other providers | Relay JSON-RPC requests to the network | Provide identical reliability across providers |
The table is not architecture decoration. It is a boundary map. Break it, and the code starts making claims it cannot enforce.
The frontend is a witness. The contract is the execution environment. The API layer is only transport and projection.
Modernizing the Web3 stack: viem vs ethers.js v6
The old default was ethers.js. That is still a defensible choice. Ethers.js v6, released in March 2023, moved toward a more modular architecture and better tree-shaking. The package is no longer the same monolith many teams remember from v5-era builds.
But viem changed the baseline. It is a TypeScript interface for Ethereum with low-level primitives for EVM chains. It is often used as a lighter alternative, with common production bundles landing around 10–20kb depending on imports and build configuration. That number is not a security property. It is a deployment constraint. Smaller code surfaces are easier to audit. Fewer abstractions mean fewer places for malformed assumptions to hide.
The important difference is not “modern versus legacy.” That framing is lazy. The difference is execution style.
Viem tends to make chain configuration explicit. It also separates public clients, wallet clients, transports, and actions in a way that forces the developer to state what is happening. That is useful in integration code. Integration code should be boring. It should say exactly which chain it targets, which transport it uses, and whether the operation is a read, a simulation, or a signed mutation.
A minimal viem read path looks like this in architectural terms:
1. Define the chain.
2. Define the transport.
3. Create a public client.
4. Read the contract with an ABI, address, function name, and arguments.
5. Treat the result as a typed observation.
The security-relevant step is not the call itself. It is the configuration. If the dapp reads from Mainnet and signs on a testnet, the UI can display coherent nonsense. If it reads from one RPC provider and broadcasts through another, transaction timing and mempool visibility can differ. If it encodes with an ABI that does not match the deployed bytecode, every downstream claim is suspect.
Ethers.js v6 can also be used safely. It provides a mature interface, signer abstractions, contract objects, and modular imports. Its risk is not that it is unsafe. Its risk is that teams often carry v5 mental models into v6 code, then mix providers, signers, BigInt handling, and contract instances without a clean execution boundary.
A practical comparison:
| Concern | viem | ethers.js v6 |
|---|---|---|
| TypeScript ergonomics | Strong typed primitives and explicit clients | Improved from v5, still often used through broader abstractions |
| Bundle behavior | Often small; commonly cited around 10–20kb for focused usage | Modular v6 supports tree-shaking when imports are disciplined |
| Execution model | Public client, wallet client, transport, actions | Provider, signer, contract-oriented model |
| Failure visibility | Explicit configuration tends to expose chain and transport assumptions | Abstractions can be clean, but can also blur read/write boundaries |
| Best fit | Teams that want deterministic integration primitives | Teams with existing ethers code or contract-object workflows |
The correct choice is the one your team can audit under pressure. Not the one with the cleaner landing page.
The call path that must be visible
For any contract read, an auditor should be able to answer five questions without running the application:
- Which chain is being queried.
- Which RPC transport is used.
- Which deployed address is targeted on that chain.
- Which ABI fragment encodes the call.
- Which block context is assumed, if any.
If that information is spread across environment variables, wallet state, default chain config, and component props, the integration is already fragile. The failure will not look like an exploit at first. It will look like a user reporting that balances are wrong.
That is usually the first symptom.
Streamlining frontend state with wagmi hooks
Wagmi exists because React applications need state, not raw RPC mechanics. It is built on viem. It provides hooks for wallet connection, account status, contract reads, transaction signing, and related flows.
This is useful. It is also dangerous when misread.
A hook is not a settlement primitive. A hook is an interface for synchronizing application state with chain-facing operations. It can cache. It can refetch. It can expose loading states. It can react to wallet changes. It cannot make asynchronous distributed state simple.
The common failure pattern is a component that treats isSuccess as finality.
A transaction submission has stages:
1. The user approves a wallet request.
2. The wallet signs and broadcasts a transaction.
3. The network accepts the transaction into mempool propagation.
4. A block includes the transaction.
5. The receipt reports execution status.
6. The application refetches the relevant state.
7. Any indexer catches up, if the UI depends on indexed events.
Only some of these stages happen inside wagmi. None should be collapsed into a single green checkmark unless the product deliberately accepts that risk.
For contract writes, the safer pattern is split execution into three separate states:
- Prepared intent: the app knows what function call it wants to request.
- Submitted transaction: a hash exists, but state is not yet reliable.
- Confirmed state mutation: the receipt is successful and direct reads or indexed projections have updated.
This is not UI pedantry. It prevents state desynchronization. It also prevents users from signing repeated transactions because the interface fails to distinguish “pending” from “failed.”
Chain identity is part of the signed message
Wallet connection libraries make chain switching feel harmless. It is not harmless. The chain ID defines the execution environment. If the frontend shows data from one chain and asks for a signature on another, the user is not approving what the UI represents.
A clean wagmi integration should enforce these properties:
- The configured chains are explicit and narrow.
- Unsupported chains fail closed.
- Contract addresses are mapped per chain, not shared through a single global constant.
- Reads are invalidated when the account or chain changes.
- Writes simulate or prepare against the same chain that will receive the transaction.
- UI state distinguishes wallet connection, network readiness, transaction submission, receipt confirmation, and refetch completion.
The invariant is plain: no component should be able to produce a transaction request without knowing the chain, address, ABI, function, arguments, and value.
If it can, the component is an attack vector.
Optimizing blockchain data querying through The Graph
Direct RPC is the wrong tool for many data retrieval jobs. It is precise for current contract state. It is inefficient for historical queries, aggregate views, and event-derived entities.
The Graph solves a specific problem: it indexes blockchain data into subgraphs and exposes it through GraphQL. That lets a frontend query entities such as positions, orders, transfers, votes, claims, or liquidations without scanning logs on demand.
This is blockchain data querying as a projection layer. Not a replacement for the chain.
Typical indexed subgraph queries can return in less than 500ms. That is useful for product interfaces. It is also why developers overtrust them. Fast data feels authoritative. It is not necessarily current. Indexers need time to ingest blocks, map events, and update entities. Reorgs and propagation delays exist. The Graph is not a real-time database.
Use it for:
- Historical event views.
- User activity tables.
- Protocol analytics.
- Derived entity state from emitted events.
- Paginated lists that would be hostile to direct RPC.
Do not use it as the only source for:
- Whether a transaction has settled.
- Whether a user can withdraw at this block.
- Whether a liquidation is valid.
- Whether a price-dependent settlement condition is satisfied.
- Whether a contract invariant currently holds.
The Graph is effective when the contract emits enough events to reconstruct the application view. That means integration quality starts in Solidity. If the contract emits vague events, the subgraph becomes a guessing engine.
A useful event for indexing is not just a notification. It is a state transition record. It should identify the entity, actor, relevant amounts, and status change. If a subgraph must infer too much from storage reads or transaction metadata, the event design is weak.
Indexers are projection engines. Treating them as consensus is an integration bug.
Design subgraphs around contract invariants
Subgraph schemas should mirror the entities the protocol actually enforces. If a lending contract tracks positions by positionId, the subgraph should not model them as loose user balances unless the protocol invariant is also user-balance based. If an auction contract has phases, the indexed entity should expose phase transitions derived from contract events, not frontend timers.
The failure mode is subtle. The UI starts presenting a model that differs from the contract model. Users act on it. Transactions revert. Support tickets appear. Then someone patches the frontend with conditional logic. Complexity grows. The invariant remains broken.
A disciplined subgraph integration has these properties:
1. Entity IDs are deterministic. They are derived from contract IDs, addresses, token IDs, or event parameters. Not from array positions.
2. Event handlers are narrow. One handler mutates the entities affected by that event. It does not rebuild unrelated state.
3. Reorg behavior is accepted. The UI does not treat fresh indexed data as final immediately after transaction submission.
4. Direct reads confirm critical actions. Before a user signs a value-bearing transaction, the app verifies critical state through RPC where practical.
5. Pagination is explicit. Large queries do not fetch unbounded entity sets into the browser.
This is how to use a blockchain API without converting every user session into a load test against an RPC provider.
Bridging off-chain data with Chainlink oracle networks
Some data does not exist on-chain until someone brings it there. Asset prices are the common example. Chainlink Data Feeds are the standard mechanism many contracts use for this job. They provide off-chain data to smart contracts through decentralized oracle networks.
The integration mistake is to treat oracle data as a frontend API problem. It is not.
If a contract uses a Chainlink price feed, the contract must read and validate that feed. The frontend can display the same feed for user clarity, but it cannot supply the price to the contract as if it were trusted input. User-provided price data is not oracle data. It is calldata. Calldata is adversarial by default.
A contract that consumes an oracle feed needs explicit checks:
- The feed address is correct for the chain.
- The answer is positive when the domain requires positive values.
- The round data is not stale under the protocol’s tolerance.
- The decimals are handled without silent truncation.
- The resulting value cannot overflow downstream arithmetic.
- The protocol defines what happens when the feed is unavailable or stale.
This is the difference between displaying market data and securing settlement.
Frontend blockchain API integration still matters here. The UI should show users the price source, update cadence where relevant, and the last observed value. But the UI is not part of the trust boundary. If the UI disappears, the contract should still enforce the same invariant.
Oracle reads and interface symmetry
A clean product often reads the same oracle-facing values in two places:
- The contract reads the feed during execution.
- The frontend reads the contract or feed for display.
Those reads must be semantically aligned. If the contract uses one decimal normalization path and the frontend uses another, the displayed liquidation threshold can differ from the executed one. If the frontend reads a cached backend price while the contract reads Chainlink, the user interface becomes a divergent market.
The fix is not more comments. The fix is shared definitions and narrow formatting rules.
Use integer math in the contract. Expose normalized values where useful. Format only at the edge. Avoid letting the frontend become the place where protocol math is “completed.” Frontend math is for presentation. Contract math is for enforcement.
Managing RPC provider limits and request efficiency
RPC providers are infrastructure. They are also rate limiters. Free tiers from major providers often sit in broad ranges such as 100k–300k requests per day, depending on the provider and current plan. Exact tiers change. The principle does not.
A dapp that calls balanceOf, allowance, decimals, symbol, totalSupply, and three protocol views for every rendered card will hit limits before it hits product-market fit. Worse, it will fail unevenly. One user sees data. Another gets timeouts. A third signs against stale state.
RPC efficiency is a correctness issue.
The first optimization is not batching. It is deletion. Many calls do not need to exist.
Token metadata can be cached. Static contract configuration can be loaded once per chain and version. Lists should be indexed. Historical data should come from a subgraph. Contract reads should be scoped to the connected account and visible UI state. Refetch intervals should match risk, not anxiety.
A reasonable request policy looks like this:
| Data type | Preferred source | Refresh model | Reason |
|---|---|---|---|
| Current allowance before spend | Direct RPC read | On account, chain, token, spender change; before signing | Critical to transaction correctness |
| Token symbol and decimals | Cached RPC read or static registry for known assets | Rarely | Mostly static; avoid repeated calls |
| User activity history | The Graph subgraph | Query on view load and pagination | Event-derived and expensive via RPC |
| Transaction receipt | RPC provider | Poll until mined or timeout | Canonical execution evidence |
| Price used in settlement | Contract oracle read | Before display and before action, where relevant | Must match contract trust path |
| Protocol analytics | Subgraph or backend cache | Periodic | Not execution-critical |
The second optimization is deduplication. React makes accidental duplication easy. Two components ask for the same balance. Three hooks refetch on focus. A modal mounts and repeats the parent’s calls. The provider sees a burst. The user sees a spinner.
Use shared query keys. Use caching intentionally. Use multicall where appropriate. Do not poll every block unless the state actually changes every block and the user benefits from seeing it. Most interfaces do not need block-speed updates.
Provider diversity without pretending providers are identical
Multiple RPC providers can reduce outage risk. They can also introduce inconsistent reads if used carelessly. Providers may differ in latency, indexing of archive data, mempool behavior, supported methods, and rate limits. They do not offer identical reliability.
Fallback logic must be explicit. If provider A fails, provider B can serve a read. But the application should avoid mixing responses from different providers inside a single critical decision unless it has a reconciliation strategy.
For transaction submission, the stakes change. Broadcasting through multiple endpoints can improve propagation in some systems, but it can also complicate nonce tracking and error handling. Nonce management must remain deterministic. Duplicate submissions, replacement transactions, and stuck pending states are not UI edge cases. They are state-machine failures.
A minimal provider policy should state:
- Which provider is primary per chain.
- Which methods are allowed on fallback providers.
- Whether archive reads are required.
- How rate-limit errors are handled.
- How transaction polling timeouts are surfaced.
- Whether the app retries automatically or requires user action.
Silent retries are acceptable for idempotent reads. They are dangerous for writes. A write path should never hide the difference between “not submitted,” “submitted but pending,” “submitted and reverted,” and “submitted and confirmed.”
How to use a blockchain API without corrupting state
The safest integration code is not clever. It is explicit.
A production blockchain API path should be modeled as a state machine. Not as a cluster of hooks and callbacks. The machine does not need to be formal in every app, but the states must exist in the code.
For a value-bearing transaction, the path is:
1. Resolve chain context. Refuse unsupported chains. Load addresses from chain-specific configuration.
2. Resolve account context. Bind reads and writes to the connected account. Invalidate on account change.
3. Read current state. Fetch balances, allowances, protocol limits, or position state from the correct source.
4. Simulate or prepare the call. Use the same ABI, address, function, arguments, value, and chain intended for execution.
5. Request signature. Present the wallet with the exact transaction intent.
6. Track submission. Store the transaction hash separately from confirmed state.
7. Confirm receipt. Check status. Handle reverts as first-class outcomes.
8. Refetch canonical reads. Update direct contract state.
9. Refresh indexed projections. Let subgraph views catch up without pretending they are immediate.
Each step has a failure branch. If the code does not represent the branch, the UI will invent one.
This is where many integrations lose determinism. They use one boolean for loading, one for error, one for success. That is insufficient. A rejected signature is not a reverted transaction. A rate-limited read is not an empty balance. A stale subgraph is not a protocol failure. A wrong chain is not a missing wallet.
The error taxonomy matters because recovery differs.
- Wrong chain: request switch or fail closed.
- User rejects signature: return to prepared state.
- RPC rate limit: back off and retry reads; do not fabricate zeros.
- Transaction reverted: show receipt failure and decode reason where possible.
- Indexer lag: show pending projection state; do not claim final indexed history.
- Oracle stale condition: block the action if the contract would reject or the risk model requires it.
No abstraction removes this. It can only hide it.
The integration invariants that matter
The final shape of a Web3 API integration should be judged by invariants, not by library preference.
Use viem if explicit primitives and smaller focused bundles help the team keep execution paths visible. Use ethers.js v6 if its contract and signer model fits the codebase and imports remain disciplined. Use wagmi when React state needs a controlled bridge to wallets and contract calls. Use The Graph for indexed historical and derived data. Use Chainlink Data Feeds for contract-consumed off-chain data, not frontend-provided settlement input.
Then enforce the hard guarantees:
- No write without explicit chain identity.
- No shared contract address across chains unless the deployment is intentionally identical and verified.
- No critical user action based only on indexed data.
- No frontend-supplied price treated as trusted settlement data.
- No silent conversion between oracle decimals, token decimals, and display decimals.
- No automatic retry that can create a second transaction without user intent.
- No RPC error converted into a valid zero value.
- No transaction marked complete before receipt confirmation and state refetch.
- No hook state treated as consensus.
- No provider fallback without a defined method policy.
Blockchain API integration is not a connectivity task. Connectivity is the easy part. The hard part is preserving the boundary between observation, projection, signing, and execution.
When that boundary is clean, the dapp behaves deterministically under load, latency, reorgs, wallet switches, and provider failures. When it is not clean, the interface becomes a second protocol. Unspecified. Unaudited. Usually wrong.
FAQ
Should I use viem or ethers.js v6 for my project?
Can I use The Graph to check if a transaction has settled?
How should I handle price data from Chainlink in my frontend?
Why does my dapp hit RPC rate limits so quickly?
What is the risk of using multiple RPC providers?
By Caleb North