blockchainsv
Web3 Integration & APIs·July 17, 2026·13 min read

What is Web3 development and is the architectural shift worth it?

The architectural shift to Web3 is rarely justified by ideology alone. In practice, teams hit a specific inflection point: a backend service that requires shared, verifiable state, or a product where the user — not the platform — holds the signing key.

What is Web3 development and is the architectural shift worth it?

Once that requirement lands on the engineering roadmap, the conversation stops being philosophical and becomes a stack decision. That decision exposes a chain of bottlenecks — RPC latency, wallet state machines, indexer freshness, oracle staleness — that simply do not exist in conventional web development. Whether that complexity is worth absorbing depends on what you are actually trying to build, and whether the guarantees Web3 offers align with the product's real trust boundaries.

The Anatomy of a Dapp: Beyond the Smart Contract

A dapp is, in its most compact form, a smart contract combined with a frontend interface. That definition is accurate and also deeply misleading. The smart contract is the smallest piece of the system. Around it sits an entire stack of infrastructure that the term "decentralized" tends to obscure.

An Ethereum application does not talk to a blockchain directly; it talks to an Ethereum node. Every read of onchain state — token balances, contract storage, event logs — flows through a node's JSON-RPC interface, and every state-changing transaction is submitted through the same surface. Ethereum clients expose a uniform set of methods across implementations, which is precisely what makes the JSON-RPC layer the actual integration boundary. Your application is not integrating with Ethereum. It is integrating with a node.

That distinction matters because the node is, in most production setups, a third-party service. RPC providers run the hardware, expose the endpoints, and price the request volume. The smart contract may be trustless, but the path to it usually is not. In production, you authenticate against a private RPC URL rather than relying on public endpoints, and you do so because public endpoints rate-limit unpredictably and offer no SLA. The architectural shift, in other words, moves the trust boundary — it does not eliminate it.

Connecting the Frontend: JSON-RPC and EIP-1193 Standards

The interface between a dapp frontend and a wallet is standardized by EIP-1193, which defines a uniform JavaScript Ethereum Provider API. A provider exposes a single method, request({ method, params }), through which RPC calls flow, and it must emit a defined set of events: connect, disconnect, chainChanged, and accountsChanged. That contract is what makes wallets interoperable across dapps and dapps interoperable across wallets; without it, every integration would be a custom adapter against a specific wallet vendor.

The same specification defines a taxonomy of error codes that, in practice, every production dapp must handle:

Error codeMeaningPractical handling
4001User rejected requestShow a neutral "transaction cancelled" state; do not retry automatically
4100Unauthorized requestPrompt reconnection or re-authorization of the account
4200Unsupported methodFall back or surface the limitation to the user
4900Provider disconnected from all chainsAttempt reconnection; disable write actions until restored
4901Provider disconnected from the requested chainSwitch chains or prompt the user to do so

Treating these as edge cases is a common mistake. They are the steady state of any production wallet integration. Users reject transactions, switch networks, and disconnect accounts constantly. A frontend that does not model the EIP-1193 event lifecycle will eventually submit a transaction against a stale account or a disconnected provider, and the failure will surface as an opaque error rather than a recoverable state.

A Web3 frontend is not a UI on top of a database. It is a UI on top of an asynchronous, stateful connection that the user controls — and can revoke at any moment.

Modern Tooling: From Viem Primitives to Wagmi Hooks

The client-library layer has consolidated around two tools: ethers (mature, broad adoption) and viem (TypeScript-first, stateless primitives). In ethers v6, the split between Provider and Signer is the core architectural distinction. A contract connected to a Provider executes read-only operations — no signing, no gas, no onchain state change. A contract connected to a Signer can submit state-changing transactions, which cost the account ether and must be awaited with tx.wait() to confirm inclusion. That two-tier model maps directly onto the read/write split you actually want in a UI: most views are read calls; mutations are deliberate, user-triggered actions.

viem takes a different tack. It is built around low-level, stateless primitives, and its basic public-client setup requires you to select both a chain and a transport explicitly. The documentation is direct about production posture: use an authenticated RPC provider URL, not the default public endpoint. That is not a style preference; it is the difference between a demo and a service.

On top of viem sits wagmi, which exposes more than twenty React hooks for accounts, wallets, contracts, transactions, signing, and ENS. A typical Ethereum.org React example imports useAccount, useReadContract, useWriteContract, useWatchContractEvent, and useSimulateContract. The architectural payoff is that wagmi wires the EIP-1193 event lifecycle, connection state, and cache invalidation into the React render cycle — so you do not reimplement those concerns on every component. Conversely, the cost is one more abstraction layer between your UI and the underlying RPC call, which means debugging occasionally requires dropping down to viem or even to raw fetch against the RPC endpoint.

The choice between ethers and viem is not tribal; it is contextual. If your team values API stability, broad ecosystem compatibility, and a long track record in production, ethers remains a reasonable default. If you are starting a TypeScript-heavy project, want tree-shakeable bundles, and prefer composable primitives over a higher-level abstraction, viem is the more ergonomic foundation. wagmi belongs in either case if your frontend is React.

Data Retrieval Strategies: Oracles and Subgraph Indexing

Once a dapp has more than trivial onchain activity, reading state directly from JSON-RPC becomes a bottleneck. Node queries for filtered event logs across thousands of blocks are slow and expensive, and the more you scan, the worse the latency story gets. Two patterns dominate the response: oracles for external data, and subgraphs for indexed onchain data.

Chainlink Data Feeds are the canonical oracle pattern. A consumer contract implements AggregatorV3Interface and calls latestRoundData() on a feed proxy. The returned answer is an integer — not a human-readable price. Applying decimal precision is the consumer's responsibility, and that precision varies by feed: the Chainlink Sepolia BTC/USD tutorial uses 8 decimals, but other pairs and networks use different values. Hard-coding a decimal count across networks is a recurring source of off-by-magnitude bugs in production.

Three additional concerns rarely surface in tutorials but matter in deployment:

1. Heartbeat and staleness. Aggregator feeds carry update-trigger metadata — a heartbeat (the cadence at which a feed is expected to update under normal conditions), a deviation threshold (the percentage move that triggers an update), or both — and which of these are present, and with what values, is feed-specific and network-specific. There is no single global staleness rule. Defining what counts as "too old" is an application-level decision: the consumer reads updatedAt from latestRoundData(), compares it against a trusted time source, and decides whether the price is fresh enough for the action it is pricing. A liquidation engine on a liquid pair may require sub-minute freshness; a settlement layer that finalizes hourly may tolerate wider windows with no real risk of being arbitraged. The right tolerance depends on the consumer's risk model, not on a property the feed guarantees universally.

2. Feed address pinning. Feed addresses are network- and pair-specific. A testnet deployment that ships a mainnet address will silently read garbage or revert on first call.

3. Round ID consistency. latestRoundData() returns a roundID that should be monotonically increasing per feed; consumers that track historical prices need to validate this rather than assume the array is sorted.

For onchain data that is not external — token transfers, contract events, historical ownership, holder lists — the standard answer is The Graph. A Subgraph is a custom open API that extracts, processes, and stores blockchain data for GraphQL querying. The subgraph.yaml manifest specifies the contracts to index, the network, the events of interest, and the mappings from event data to stored entities. The Graph generates read-only query endpoints; it does not expose mutations, which is the correct boundary for a frontend data layer.

The operational concern with subgraphs is freshness. A Subgraph does not necessarily reflect the chain head — there is always an indexing lag, sometimes a small one, occasionally a large one tied to chain reorganizations or subgraph re-syncing. The _meta query exposes the latest indexed block and whether the Subgraph has indexing errors, which means a frontend can inspect data freshness and indexing health before rendering numbers that look authoritative but are stale by thousands of blocks. In practice, any UI that displays Subgraph data should show a "last updated" timestamp sourced from _meta, not from the client's local clock, and should gracefully degrade when the indexed block falls behind expected finality.

Direct JSON-RPC, custom indexers, and other data services are still appropriate depending on query patterns and consistency requirements. The decision matrix roughly breaks down as:

Query shapeBest fitReason
Single contract reads, low latencyDirect RPC (ethers / viem)No indexing overhead; result is current as of node head
Filtered event history, aggregated viewsSubgraph (The Graph)Indexer absorbs scan cost; GraphQL composes cleanly
External price / reference dataOracle (Chainlink)Signed, decentralized source with documented update cadence
Offchain data not on any oracleCustom backend or second-layer oracleSubgraphs cannot index what is not onchain

The Reality of Decentralization: Infrastructure Trade-offs

The hardest part of Web3 development is not writing the contract. It is keeping the rest of the stack honest about what it actually is.

A frontend may be hosted on IPFS, which gives the application content addressing: the file's hash is its identifier, and changing the file changes the hash. That immutability is the property; it is also the operational constraint. An "IPFS-hosted website" is not a continuously updated deployment target. Browser access usually flows through an HTTP gateway, and how that gateway exposes content matters for browser security. Path-based gateways serve every CID under one hostname at a path like /ipfs/<CID>, which means content from unrelated sites shares an origin — that erodes the same-origin guarantees browsers rely on for cookies, storage, and permissions, and it is the reason browsers warn against persisting state on those URLs. Subdomain gateways fix the origin problem differently: by serving each CID from a distinct host such as <CID>.ipfs.gateway.tld, the DNS layer places every site at its own browser origin, and origin isolation is restored. That per-CID origin routing is what the term "subdomain gateway" actually refers to.

DNSLink is a separate convention layered on top of all of this, and it is worth not conflating the two. DNSLink is a naming and routing bridge between human-readable domains and content identifiers, not an origin-isolation mechanism: a DNS TXT record — conventionally set on _dnslink.example.com — points to a /ipfs/<CID> path (or its /ipns/ equivalent), and any DNSLink-aware resolver serving the apex domain then routes requests for example.com to that content. A normal-looking domain ends up delivering IPFS content. The browser origin the user actually lands on depends on how the resolver terminates the request — through a path-based gateway (still one shared origin) or a subdomain gateway (per-CID origin) — so origin isolation is determined by the gateway configuration, not by DNSLink itself. The practical takeaway is that "IPFS-hosted" and "served under a normal domain" are two different choices, and they compose in ways the marketing tends to flatten into one thing.

The broader architectural reality is uncomfortable but worth stating plainly. RPC providers, wallet software, indexers, gateways, DNS, analytics, and deployment pipelines can — and routinely do — remain centralized or federated even when the smart contract itself is trustless. A dapp that relies on a single RPC provider, a single subgraph hosted on a centralized service, and a single gateway is, in operational terms, a three-point-of-failure system. The cryptographic guarantees of the contract do not propagate to those layers automatically. They have to be designed, redundant, and monitored like any other production dependency.

Decentralization is not a binary you toggle. It is a series of trust boundaries, each of which you can move closer to or further from the user. The architectural shift is worth it only where at least one of those boundaries actually matters to the product.

When the Architectural Shift Is Worth It

The decision is not whether Web3 is "better" than Web2. It is whether the properties a blockchain provides — shared verifiable state, user-controlled signing, censorship-resistant execution — are load-bearing for the product, or whether they are decorative.

The shift is justified when multiple parties need to agree on a single source of truth without trusting a central operator; when the user, not the platform, must custody assets or credentials; or when the product's value proposition depends on exit — users can leave with their data, identity, or assets intact and verifiable elsewhere.

Conversely, the shift is not justified when the product is a single-vendor service with no shared state; when the user has no reason to hold keys and custodial UX would actually serve them better; or when the "decentralization" is a marketing layer over a fully centralized backend.

In practice, most well-engineered Web3 products sit at specific, defensible boundaries: a contract that holds assets and enforces rules without operator discretion, an RPC layer that is multi-provider with failover and authenticated endpoints, an indexer that is either decentralized or redundantly hosted, and a frontend that is either pinned to IPFS with a CID-addressed deployment pipeline or served from a CDN with a recoverable fallback to the CID. None of those layers is automatically decentralized. Each one is a deliberate engineering decision, and each one has a cost.

Closing Position

Web3 development is the practice of building applications that read from and write to blockchain networks through smart contracts, JSON-RPC endpoints, wallet providers, and client libraries. The architectural shift is worth it when shared, verifiable state or user-controlled signing is a load-bearing requirement, and when the team is prepared to engineer every adjacent layer — RPC, indexer, oracle, gateway — with the same rigor it would apply to any production backend.

It is not worth the operational complexity if the only reason for the shift is narrative alignment. A dapp with a single-vendor RPC, a single hosted indexer, and a custodial wallet is not a decentralized application; it is a conventional product with extra steps. The honest version of Web3 development accepts that decentralization is a property you build layer by layer, not a label you attach at the end.

For teams evaluating the shift: start with the trust boundary that actually matters to your product, choose the minimal stack that supports it, and resist the urge to add infrastructure for its own sake. The bottleneck you solve first is usually the one that determines whether the rest of the architecture is worth building at all.

FAQ

Why should I use an authenticated RPC provider instead of a public endpoint?
Public endpoints often suffer from unpredictable rate-limiting and lack service level agreements (SLAs), making them unsuitable for production environments.
What is the difference between ethers and viem for Web3 development?
Ethers is a mature library with broad adoption and a clear Provider/Signer model, while viem is a TypeScript-first, stateless library that offers more ergonomic, composable primitives.
How do I handle data freshness when using The Graph for my dapp?
You should inspect the _meta query to check the latest indexed block and indexing health, and display a 'last updated' timestamp to the user to account for potential indexing lag.
Why is it important to handle EIP-1193 error codes in a frontend?
These error codes represent the steady state of wallet interactions, such as users rejecting transactions or switching networks; failing to model these events leads to opaque errors and broken user flows.
What are the risks of using Chainlink oracles in a smart contract?
Risks include varying decimal precision across networks, potential staleness of data, and the need for the consumer to define its own risk model for what constitutes an acceptable update cadence.

By Lucas Meade