Building a DeFi DApp Frontend
Why the Frontend Matters
Smart contracts are invisible. Nobody can "see" a Solidity contract running on the blockchain. The DApp frontend is the bridge — it is what users actually see, click, and interact with. Without a well-built frontend, your DeFi protocol might as well not exist.
In this final chapter, we will build a complete DApp frontend that connects to MetaMask, reads token balances, sends transactions, and displays all of our smart contracts in a beautiful, usable interface.
Why this matters for your career:
- A DApp without a frontend has zero users — frontend development is essential for any Web3 product
- Understanding the full stack (contract → frontend → user) is what separates a developer from an engineer
- Companies hiring Web3 developers universally require ethers.js and wallet integration experience
Architecture Overview
User's Browser
│
├── MetaMask Wallet (manages private keys & signing)
│
▼
React / Next.js DApp
│
├── ethers.js (communicates with the blockchain)
│
▼
Ethereum Blockchain (executes smart contracts)
The flow is simple:
- MetaMask holds the user's private key and signs transactions
- ethers.js reads data from the blockchain and sends signed transactions
- The React UI displays real-time data and provides clickable buttons for every action
Step 1: Install Dependencies
# In your Next.js project
npm install ethers
That is it. ethers.js is the only library you need — it handles provider connections, wallet signing, contract interaction, event listening, and transaction management.
Step 2: Build the Wallet Connection Context
// context/Web3Context.jsx
"use client";
import { createContext, useContext, useState, useEffect } from 'react';
import { ethers } from 'ethers';
const Web3Context = createContext();
export function Web3Provider({ children }) {
const [account, setAccount] = useState(null);
const [provider, setProvider] = useState(null);
const [signer, setSigner] = useState(null);
const [chainId, setChainId] = useState(null);
const [isConnecting, setIsConnecting] = useState(false);
const [error, setError] = useState(null);
const connectWallet = async () => {
if (!window.ethereum) {
setError('Please install MetaMask!');
return;
}
setIsConnecting(true);
setError(null);
try {
const provider = new ethers.BrowserProvider(window.ethereum);
const accounts = await provider.send('eth_requestAccounts', []);
const signer = await provider.getSigner();
const network = await provider.getNetwork();
setProvider(provider);
setSigner(signer);
setAccount(accounts[0]);
setChainId(network.chainId);
// Listen for account changes
window.ethereum.on('accountsChanged', (accounts) => {
if (accounts.length === 0) {
setAccount(null);
setSigner(null);
} else {
setAccount(accounts[0]);
}
});
// Listen for chain changes
window.ethereum.on('chainChanged', (chainId) => {
setChainId(Number(chainId));
});
} catch (err) {
setError(err.message);
} finally {
setIsConnecting(false);
}
};
const disconnectWallet = () => {
setAccount(null);
setSigner(null);
setProvider(null);
};
return (
<Web3Context.Provider value={{
account,
provider,
signer,
chainId,
isConnecting,
error,
connectWallet,
disconnectWallet,
}}>
{children}
</Web3Context.Provider>
);
}
export function useWeb3() {
return useContext(Web3Context);
}
This context provides every component in your DApp with access to the wallet state — account address, signer, provider, connection status, and wallet actions.
Step 3: Build the Token Dashboard Component
// components/TokenDashboard.jsx
"use client";
import { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import { useWeb3 } from '@/context/Web3Context';
// VibeToken ABI (only the functions we need)
const VIBE_TOKEN_ABI = [
"function balanceOf(address) view returns (uint256)",
"function transfer(address to, uint256 amount) returns (bool)",
"function approve(address spender, uint256 amount) returns (bool)",
"function allowance(address owner, address spender) view returns (uint256)",
"function totalSupply() view returns (uint256)",
"function name() view returns (string)",
"function symbol() view returns (string)",
"event Transfer(address indexed from, address indexed to, uint256 value)"
];
export default function TokenDashboard({ tokenAddress }) {
const { account, signer } = useWeb3();
const [balance, setBalance] = useState('0');
const [totalSupply, setTotalSupply] = useState('0');
const [tokenName, setTokenName] = useState('');
const [tokenSymbol, setTokenSymbol] = useState('');
const [transferTo, setTransferTo] = useState('');
const [transferAmount, setTransferAmount] = useState('');
const [txStatus, setTxStatus] = useState('');
useEffect(() => {
if (account && signer && tokenAddress) {
loadTokenInfo();
}
}, [account, signer, tokenAddress]);
const getTokenContract = () => {
return new ethers.Contract(tokenAddress, VIBE_TOKEN_ABI, signer);
};
const loadTokenInfo = async () => {
try {
const contract = getTokenContract();
const balance = await contract.balanceOf(account);
const totalSupply = await contract.totalSupply();
const name = await contract.name();
const symbol = await contract.symbol();
setBalance(ethers.formatEther(balance));
setTotalSupply(ethers.formatEther(totalSupply));
setTokenName(name);
setTokenSymbol(symbol);
} catch (err) {
console.error('Failed to load token info:', err);
}
};
const handleTransfer = async () => {
if (!transferTo || !transferAmount) return;
setTxStatus('Transaction in progress...');
try {
const contract = getTokenContract();
const amount = ethers.parseEther(transferAmount);
const tx = await contract.transfer(transferTo, amount);
setTxStatus(`Transaction submitted! Waiting for confirmation... TxHash: ${tx.hash}`);
const receipt = await tx.wait();
setTxStatus(`✅ Transfer confirmed! Block: ${receipt.blockNumber}`);
// Refresh balance
loadTokenInfo();
} catch (err) {
setTxStatus(`❌ Transfer failed: ${err.message}`);
}
};
return (
<div className="glass p-8 rounded-3xl border border-primary/20">
<h2 className="text-2xl font-bold mb-6">💰 {tokenName || 'Token'} Dashboard</h2>
<div className="grid grid-cols-2 gap-4 mb-8">
<div className="bg-green-500/10 p-4 rounded-2xl border border-green-500/20">
<p className="text-green-400 text-sm mb-1">Your Balance</p>
<p className="text-3xl font-bold">{Number(balance).toFixed(4)}</p>
<p className="text-green-400/60 text-sm">{tokenSymbol}</p>
</div>
<div className="bg-blue-500/10 p-4 rounded-2xl border border-blue-500/20">
<p className="text-blue-400 text-sm mb-1">Total Supply</p>
<p className="text-3xl font-bold">{Number(totalSupply).toLocaleString()}</p>
<p className="text-blue-400/60 text-sm">{tokenSymbol}</p>
</div>
</div>
{/* Transfer Form */}
<div className="space-y-4">
<h3 className="font-semibold">Transfer {tokenSymbol}</h3>
<input
type="text"
placeholder="Recipient Address (0x...)"
value={transferTo}
onChange={(e) => setTransferTo(e.target.value)}
className="w-full bg-background border border-border rounded-xl px-4 py-3"
/>
<input
type="number"
placeholder="Amount"
value={transferAmount}
onChange={(e) => setTransferAmount(e.target.value)}
className="w-full bg-background border border-border rounded-xl px-4 py-3"
/>
<button
onClick={handleTransfer}
className="w-full bg-primary text-primary-foreground py-3 rounded-xl font-bold hover:bg-blue-600"
>
Send {tokenSymbol}
</button>
{txStatus && (
<p className="text-sm text-foreground/60 mt-2">{txStatus}</p>
)}
</div>
</div>
);
}
Step 4: Build the Wallet Connection Button
// components/WalletButton.jsx
"use client";
import { useWeb3 } from '@/context/Web3Context';
export default function WalletButton() {
const { account, isConnecting, error, connectWallet, disconnectWallet } = useWeb3();
if (account) {
return (
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 bg-green-500/10 px-4 py-2 rounded-full border border-green-500/20">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
<span className="text-sm font-medium">
{account.slice(0, 6)}...{account.slice(-4)}
</span>
</div>
<button
onClick={disconnectWallet}
className="text-sm text-foreground/60 hover:text-foreground"
>
Disconnect
</button>
</div>
);
}
return (
<div>
<button
onClick={connectWallet}
disabled={isConnecting}
className="bg-primary text-primary-foreground px-6 py-2 rounded-full font-semibold hover:bg-blue-600 disabled:opacity-50"
>
{isConnecting ? 'Connecting...' : '🦊 Connect MetaMask'}
</button>
{error && (
<p className="text-red-500 text-sm mt-2">{error}</p>
)}
</div>
);
}
Step 5: Assemble the DApp Page
// app/dapp/page.jsx
"use client";
import { Web3Provider, useWeb3 } from '@/context/Web3Context';
import WalletButton from '@/components/WalletButton';
import TokenDashboard from '@/components/TokenDashboard';
function DAppContent() {
const { account, chainId } = useWeb3();
// Sepolia testnet chain ID
const SEPOLIA_CHAIN_ID = 11155111;
const isCorrectNetwork = chainId === SEPOLIA_CHAIN_ID;
return (
<div className="max-w-4xl mx-auto px-6 py-12">
<div className="flex items-center justify-between mb-12">
<div>
<h1 className="text-4xl font-bold mb-2">🔗 Vibe DApp</h1>
<p className="text-foreground/60">
Connect your wallet to explore the decentralized world
</p>
</div>
<WalletButton />
</div>
{!account ? (
<div className="text-center py-20">
<p className="text-2xl mb-4">🦊</p>
<p className="text-xl text-foreground/60">Click the button above to connect MetaMask</p>
</div>
) : !isCorrectNetwork ? (
<div className="glass p-8 rounded-3xl border border-yellow-500/20 text-center">
<p className="text-2xl mb-4">⚠️</p>
<p className="text-xl font-bold mb-2">Please switch to Sepolia testnet</p>
<p className="text-foreground/60 mb-4">
Current network Chain ID: {chainId}
</p>
<button
onClick={async () => {
try {
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: '0xaa36a7' }], // Sepolia (11155111)
});
} catch (err) {
console.error('Network switch failed:', err);
}
}}
className="bg-primary text-primary-foreground px-6 py-3 rounded-xl font-semibold"
>
Switch to Sepolia
</button>
</div>
) : (
<div className="grid gap-8">
<TokenDashboard tokenAddress={process.env.NEXT_PUBLIC_TOKEN_ADDRESS} />
{/* More DeFi features coming soon */}
<div className="glass p-8 rounded-3xl border border-primary/20">
<h2 className="text-2xl font-bold mb-4">🚧 More DeFi Features</h2>
<div className="grid grid-cols-2 gap-4">
<div className="bg-purple-500/10 p-6 rounded-2xl border border-purple-500/20 hover:border-purple-500/40 transition-colors cursor-pointer">
<p className="text-3xl mb-2">🏊</p>
<p className="font-semibold">Liquidity Pool</p>
<p className="text-sm text-foreground/60">Provide liquidity and earn fees</p>
</div>
<div className="bg-blue-500/10 p-6 rounded-2xl border border-blue-500/20 hover:border-blue-500/40 transition-colors cursor-pointer">
<p className="text-3xl mb-2">🏦</p>
<p className="font-semibold">Lending Protocol</p>
<p className="text-sm text-foreground/60">Collateralize assets to borrow tokens</p>
</div>
</div>
</div>
</div>
)}
</div>
);
}
export default function DAppPage() {
return (
<Web3Provider>
<DAppContent />
</Web3Provider>
);
}
How to Use Vibe Coding to Build the DApp Frontend
🔥 Vibe Coding Prompt for DApp Frontend "Build a complete DeFi DApp frontend with these requirements:
- Use React + Next.js + Tailwind CSS with a dark theme
- Connect MetaMask wallet, display the connected account address
- Support switching between Ethereum networks (mainnet / Sepolia)
- Token dashboard: show balance, total supply, and a transfer form
- NFT gallery: show the user's NFT collection with images
- Glassmorphism design style with smooth animations"
Summary
In this chapter, you built a complete DApp frontend that connects your smart contracts to real users:
- What you built: A React DApp with wallet connection, token dashboard, network switching, and transfer functionality
- Why it matters: Smart contracts are invisible — the frontend is what users actually see and use
- How it works: ethers.js creates a bridge between the React UI and the blockchain; MetaMask handles signing; the DApp sends read calls and write transactions seamlessly
Key takeaways:
- The Web3Context pattern provides wallet state to your entire DApp without prop drilling
- ethers.js
BrowserProviderwraps MetaMask — no RPC URL needed, MetaMask provides the connection - Always check the user is on the correct network (Sepolia for testing, mainnet for production)
- Monitor
accountsChangedandchainChangedevents to react to wallet changes in real time - The ABI is your contract's API — only include the functions you need
- Every transaction goes through a lifecycle: submit → pending → confirmed → update UI
Congratulations — You Have Completed the Entire Course!
You now possess the complete skill set of a Web3 developer:
- ⛓️ Blockchain fundamentals: How decentralization, consensus, wallets, and gas work
- 📝 Solidity programming: Write, compile, and deploy smart contracts
- 💰 ERC-20 tokens: Launch your own cryptocurrency with taxes, minting, and burning
- 🎨 ERC-721 NFTs: Create unique digital assets with metadata, reveals, and royalties
- 🔄 Decentralized Exchange: Build an AMM-based DEX with liquidity pools and swaps
- 🏦 Lending Protocol: Implement collateralized lending with liquidations and interest
- 🌐 DApp Frontend Development: Connect MetaMask, interact with contracts, build beautiful UIs
These skills will serve you whether you are:
- Applying for Web3 developer positions at protocols, exchanges, or startups
- Launching your own token or NFT project
- Freelancing as a blockchain developer
- Building the next generation of decentralized applications
The blockchain revolution is just getting started. You are now equipped to be part of it.