dApp Development: Building Full-Stack Decentralized Applications
In this tutorial, you'll learn full-stack dApp development including smart contracts, React frontend integration, wallet connection, and production deployment. Why it matters: dApps are the foundation of Web3, enabling trustless, transparent, and censorship-resistant applications that serve millions of users across DeFi, gaming, and social platforms. By the end, you'll build and deploy a complete dApp from contract to UI.
A dApp (decentralized application) combines one or more smart contracts with a frontend that interacts with them, running on a blockchain network instead of traditional centralized servers.
Project Architecture
A typical dApp consists of three layers: the blockchain (smart contracts), the connection layer (ethers.js/Web3.js), and the frontend (React/Next.js).
flowchart TD A[User Browser] --> B[React Frontend] B --> C[ethers.js / Web3.js] C --> D[Wallet
MetaMask / WalletConnect] D --> E[Blockchain Node
Infura / Alchemy] E --> F[Smart Contract
Ethereum / Polygon] B --> G[IPFS
Decentralized Storage] F --> H[The Graph
Event Indexing] B -- "Call ()" --> F B -- "Send ()" --> F B -- "Read Events" --> H
Setting Up the Project
Create a monorepo with a Hardhat backend and React frontend.
mkdir my-dapp && cd my-dapp
mkdir contracts frontend
# Initialize smart contract project
cd contracts
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npx hardhat init
# Initialize frontend
cd ../frontend
npx create-react-app .
npm install ethers @web3-react/core @web3-react/injected-connector
Writing the Smart Contract
Create a simple decentralized voting contract.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract Voting {
struct Proposal {
uint256 id;
string name;
uint256 voteCount;
}
mapping(address => bool) public hasVoted;
Proposal[] public proposals;
bool public votingOpen;
event Voted(address indexed voter, uint256 indexed proposalId);
event VotingOpened();
event VotingClosed();
constructor(string[] memory _proposalNames) {
for (uint256 i; i < _proposalNames.length; i++) {
proposals.push(Proposal(i, _proposalNames[i], 0));
}
votingOpen = true;
}
function vote(uint256 _proposalId) external {
require(votingOpen, "Voting is closed");
require(!hasVoted[msg.sender], "Already voted");
require(_proposalId < proposals.length, "Invalid proposal");
hasVoted[msg.sender] = true;
proposals[_proposalId].voteCount++;
emit Voted(msg.sender, _proposalId);
}
function closeVoting() external {
require(votingOpen, "Already closed");
votingOpen = false;
emit VotingClosed();
}
function getProposals() external view returns (Proposal[] memory) {
return proposals;
}
function getWinner() external view returns (uint256 winningProposal, string memory winnerName) {
uint256 winningCount;
for (uint256 i; i < proposals.length; i++) {
if (proposals[i].voteCount > winningCount) {
winningCount = proposals[i].voteCount;
winningProposal = i;
winnerName = proposals[i].name;
}
}
}
}
Deploy to Hardhat network:
const { ethers } = require("hardhat");
async function main() {
const proposals = ["Alice", "Bob", "Charlie"];
const Voting = await ethers.getContractFactory("Voting");
const voting = await Voting.deploy(proposals);
await voting.waitForDeployment();
const address = await voting.getAddress();
console.log("Voting deployed to:", address);
// Save ABI and address for frontend
const fs = require("fs");
const artifact = require("./artifacts/contracts/Voting.sol/Voting.json");
fs.writeFileSync(
"../frontend/src/contracts/Voting.json",
JSON.stringify({ address, abi: artifact.abi }, null, 2)
);
}
main().catch(console.error);
Building the React Frontend
Create the React components for wallet connection and voting.
// src/App.js
import { useState, useEffect } from "react";
import { ethers } from "ethers";
import VotingArtifact from "./contracts/Voting.json";
function App() {
const [provider, setProvider] = useState(null);
const [signer, setSigner] = useState(null);
const [account, setAccount] = useState("");
const [contract, setContract] = useState(null);
const [proposals, setProposals] = useState([]);
const [hasVoted, setHasVoted] = useState(false);
async function connectWallet() {
if (window.ethereum) {
const provider = new ethers.BrowserProvider(window.ethereum);
const accounts = await provider.send("eth_requestAccounts", []);
const signer = await provider.getSigner();
setProvider(provider);
setSigner(signer);
setAccount(accounts[0]);
const contract = new ethers.Contract(
VotingArtifact.address,
VotingArtifact.abi,
signer
);
setContract(contract);
// Load proposals
const proposals = await contract.getProposals();
setProposals(proposals);
// Check if user voted
const voted = await contract.hasVoted(accounts[0]);
setHasVoted(voted);
}
}
async function castVote(proposalId) {
try {
const tx = await contract.vote(proposalId);
const receipt = await tx.wait();
if (receipt.status === 1) {
setHasVoted(true);
const updated = await contract.getProposals();
setProposals(updated);
}
} catch (error) {
alert("Vote failed: " + error.message);
}
}
return (
<div>
<h1>Decentralized Voting dApp</h1>
{!account ? (
<button onClick={connectWallet}>Connect Wallet</button>
) : (
<div>
<p>Connected: {account.slice(0, 6)}...{account.slice(-4)}</p>
<h2>Proposals</h2>
{proposals.map((proposal, i) => (
<div key={i}>
<span>{proposal.name} - {proposal.voteCount.toString()} votes</span>
{!hasVoted && (
<button onClick={() => castVote(i)}>Vote</button>
)}
</div>
))}
{hasVoted && <p>You have already voted.</p>}
</div>
)}
</div>
);
}
export default App;
// src/index.js
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Transaction Management
Handle transaction states (pending, confirming, confirmed) in the UI.
function TransactionManager({ txState, setTxState }) {
return (
<div>
{txState.status === "pending" && (
<div>
<p>Waiting for wallet confirmation...</p>
</div>
)}
{txState.status === "submitted" && (
<div>
<p>Transaction submitted!</p>
<p>Hash: {txState.hash}</p>
<a href={`https://etherscan.io/tx/${txState.hash}`} target="_blank">
View on Etherscan
</a>
<p>Waiting for confirmations...</p>
</div>
)}
{txState.status === "confirmed" && (
<div>
<p>Transaction confirmed!</p>
<p>Block: {txState.blockNumber}</p>
<p>Gas used: {txState.gasUsed}</p>
</div>
)}
{txState.status === "error" && (
<div>
<p>Transaction failed: {txState.error}</p>
</div>
)}
</div>
);
}
// Usage in App.js
async function castVote(proposalId) {
setTxState({ status: "pending" });
try {
const tx = await contract.vote(proposalId);
setTxState({ status: "submitted", hash: tx.hash });
const receipt = await tx.wait();
setTxState({
status: "confirmed",
blockNumber: receipt.blockNumber,
gasUsed: receipt.gasUsed.toString(),
});
// Reload data
const voted = await contract.hasVoted(account);
setHasVoted(voted);
const updated = await contract.getProposals();
setProposals(updated);
} catch (error) {
setTxState({ status: "error", error: error.message });
}
}
Production Build and Deployment
Deploy the smart contract to a testnet and build the frontend for production.
// hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();
module.exports = {
solidity: "0.8.19",
networks: {
sepolia: {
url: process.env.SEPOLIA_URL,
accounts: [process.env.PRIVATE_KEY],
},
},
};
# Deploy contract to Sepolia
cd contracts
npx hardhat run scripts/deploy.js --network sepolia
# Build frontend
cd ../frontend
npm run build
# Deploy frontend to Vercel or Netlify
# npx vercel --prod
Expected build output:
Creating an optimized production build...
Compiled successfully.
File sizes after gzip:
64.23 kB build/static/js/main.abc123.js
315 B build/static/css/main.def456.css
1.42 kB build/index.html
Common Errors and Misunderstandings
1. Contract State Not Updating
Frontend caches contract state. Always re-fetch after transactions. Use useEffect with transaction count as dependency.
2. Network Mismatch
Users on wrong network cause transaction failures. Check provider.getNetwork() and prompt network switch via wallet_switchEthereumChain.
3. Missing Provider Injection
Users without MetaMask see errors. Show a friendly message: "Please install MetaMask to use this dApp."
4. Gas Estimation Failures
Complex contracts may fail gas estimation. Use overrideGasLimit or estimateGas with a buffer.
5. Promises Not Handled
Async errors that are not caught crash the dApp. Wrap all wallet interactions in try-catch blocks.
Practice Questions
What are the three layers of a dApp? The smart contract layer (blockchain), the connection layer (ethers.js/Web3.js), and the frontend layer (React/Vue/Next.js).
How does MetaMask connect to a dApp? MetaMask injects an
<a href="/cryptocurrency/ethereum/">ethereum</a>provider into the browser window. The dApp useswindow.<a href="/cryptocurrency/ethereum/">ethereum</a>.request({ method: "eth_requestAccounts" })to access accounts.Why use
BrowserProviderinstead ofJsonRpcProvider?BrowserProviderwraps the injected wallet provider (MetaMask), enabling user accounts and transaction signing.JsonRpcProviderconnects directly to a node without user interaction.What is the purpose of waiting for transaction confirmations? To ensure the transaction is included in a block and won't be reverted. One confirmation means one block has been mined on top. More confirmations increase finality.
How do you handle network switching in a dApp? Use
window.<a href="/cryptocurrency/ethereum/">ethereum</a>.request({ method: "wallet_switchEthereumChain", params: [{ chainId: "0x5" }] })to prompt users to switch networks.
Challenge
Build a complete decentralized crowdfunding dApp with a smart contract that accepts ETH contributions, tracks contribution amounts per address, enforces a funding goal and deadline, allows refunds if the goal is not met, and a React frontend with real-time progress tracking using event subscriptions.
Real-World Task
Create a production-grade NFT minting dApp using Hardhat for the contract, React for the frontend, ethers.js for blockchain interaction, and IPFS via Pinata for metadata storage. Include wallet connection, minting with price and supply tracking, and collection browsing with TypeScript types.
Frequently Asked Questions
Next Steps
After building your dApp, integrate MetaMask deeply with advanced wallet interactions, explore The Graph for efficient event indexing, and deploy to Layer 2 for lower gas costs.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro