dApp (Decentralized Application) Development
A dApp differs from a regular web application not because "it uses blockchain" — but because critical business logic is executed on-chain, and the user interacts with it directly through their wallet, without an intermediary. This is a fundamentally different architecture: there's no backend server that "owns" user data, no database with balances — only smart contracts and events. Everything else is a project decision about how much off-chain infrastructure you're willing to maintain.
Architectural Decisions at the Start
Level of Decentralization
The first honest question — what should be on-chain, and what shouldn't? Every byte in a smart contract costs gas. Storing everything on-chain is expensive and often pointless.
Fully on-chain: logic + data in the contract. Suitable for financial primitives (AMM, lending, staking). No backend, works through any RPC.
Hybrid: logic on-chain, UI + indexing off-chain. 90% of dApps. The contract is the source of truth for financial operations, off-chain backend handles fast search, notifications, analytics.
Light dApp: smart contract only for payments/ownership, main functionality is a regular web service. Often the correct choice for the first version of a product.
Stack
Standard stack for 2024-2025: React 18 + TypeScript + Vite, Wagmi v2 + Viem for blockchain interaction, RainbowKit or ConnectKit for wallet connection, TanStack Query for caching on-chain data. For SSR requirements — Next.js, but carefully: server components + wagmi require careful setup.
State management: Zustand or Jotai work well for dApps (less boilerplate than Redux, combine well with reactive wagmi hooks). Recoil — if the project already uses it.
Wallet Connection and Authentication
Multi-wallet Support
Users arrive with MetaMask, Coinbase Wallet, WalletConnect, Ledger, Safe. Wagmi v2 + WalletConnect v2 covers 95% of use cases out of the box. Custom integration is rarely needed — only for corporate wallets or specific use cases.
const config = createConfig({
chains: [mainnet, polygon, arbitrum],
transports: {
[mainnet.id]: http(process.env.VITE_ALCHEMY_MAINNET_URL),
[polygon.id]: http(process.env.VITE_ALCHEMY_POLYGON_URL),
[arbitrum.id]: http(process.env.VITE_ALCHEMY_ARBITRUM_URL),
},
connectors: [
injected(),
coinbaseWallet({ appName: 'MyDApp' }),
walletConnect({ projectId: WC_PROJECT_ID }),
],
});
SIWE (Sign-In with Ethereum)
For dApps with off-chain components (profiles, settings, notifications), authentication without a password is needed. SIWE (EIP-4361): user signs a text message with nonce, backend verifies the signature, issues a JWT. This is not a transaction — the signature is free and instant.
const message = new SiweMessage({
domain: window.location.host,
address: account.address,
statement: 'Sign in with Ethereum to MyDApp.',
uri: window.location.origin,
version: '1',
chainId: chain.id,
nonce: await getNonce(), // from server, for replay protection
});
On-chain Data Fetching
Multicall and Request Batching
A naive approach — a separate RPC call for each balanceOf, allowance, userInfo. With 20 tokens — 20 requests, ~2 second delay. Multicall3 (deployed on all major networks at address 0xcA11bde05977b3631167028862bE2a173976CA11) lets you pack N calls into one:
const results = await client.multicall({
contracts: tokens.map(token => ({
address: token.address,
abi: erc20Abi,
functionName: 'balanceOf',
args: [userAddress],
})),
});
Wagmi automatically batches useReadContracts through Multicall3. But it's important to understand the limits: very large batches can exceed node gas limits.
The Graph vs. Custom Indexer
For reading historical data (events, transactions, aggregates) — you can't rely on eth_getLogs with a wide block range: nodes limit requests. Two options:
The Graph: GraphQL API over indexed events. Subgraph is written in AssemblyScript, deployed to Subgraph Studio. Good for DeFi data (TVL, volumes, positions). Indexing delay — several blocks.
Alchemy/Moralis API: managed indexing without writing a subgraph. Faster to start, more expensive to scale, less flexibility.
Custom indexer: PostgreSQL + TypeScript service listening to events via WebSocket. Full control, but infrastructure support. Recommended with specific data requirements or high loads.
Transaction UX
Transactions are the main source of friction in dApps. The user shouldn't have to guess what's happening.
Transaction States
Full lifecycle: idle → signing (waiting for signature in wallet) → pending (transaction in mempool) → confirming (N of M confirmations) → success/error. Each state requires UI feedback.
const { writeContractAsync, isPending } = useWriteContract();
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
hash: txHash,
});
Gas Estimation and EIP-1559
Wagmi/Viem uses EIP-1559 (maxFeePerGas + maxPriorityFeePerGas) by default on supporting networks. For UX: show estimated fee in USD before confirmation, using eth_estimateGas + current gas price + ETH/USD oracle (Chainlink or CoinGecko API).
Too low gasLimit — transaction fails with out-of-gas. Too high — user sees a scary number. Adding 20% buffer to estimated gas — standard practice.
Approve Flow
ERC-20 requires approve before transferFrom. Two-step process (approve → action) — source of confusion. Solutions:
- Permit (EIP-2612): one signature instead of approve transaction, if token supports it
- Unlimited approve: once per contract (good UX, bad security — not recommended)
- Exact approve: approve exactly the amount of the current operation — correct, but two transactions each time
Multichain and Network
Chain Switching
User may be connected to the wrong chain. Auto-request to switch:
const { switchChain } = useSwitchChain();
if (chain?.id !== targetChainId) {
await switchChain({ chainId: targetChainId });
}
For new networks (not in MetaMask by default) — use wallet_addEthereumChain RPC method to add.
RPC Resilience
Single RPC is a single point of failure. For production: multiple providers with fallback (Alchemy primary, Infura secondary, public RPC tertiary). Viem supports fallback transport out of the box.
Frontend Security
- No private key on frontend — obvious, but worth stating explicitly
- Verify contract addresses from env variables, not hardcoded in code
- Content Security Policy against XSS — especially critical, as XSS in dApp can lead to fund theft via fake transactions
- Check chainId in every transaction — protection from replay attacks on another chain
- ENS resolution with reverse lookup check (address → ENS → address matches)
Timeline Guidelines
MVP dApp (one chain, basic operations, wallet connect): 2-3 weeks. Full-featured product with multichain, analytics, The Graph indexing and production-ready UX: 2-3 months.







