Skip to content

dApp Development: Building Full-Stack Decentralized Applications

DodaTech Updated 2026-06-22 7 min read

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

  1. 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).

  2. How does MetaMask connect to a dApp? MetaMask injects an <a href="/cryptocurrency/ethereum/">ethereum</a> provider into the browser window. The dApp uses window.<a href="/cryptocurrency/ethereum/">ethereum</a>.request({ method: "eth_requestAccounts" }) to access accounts.

  3. Why use BrowserProvider instead of JsonRpcProvider? BrowserProvider wraps the injected wallet provider (MetaMask), enabling user accounts and transaction signing. JsonRpcProvider connects directly to a node without user interaction.

  4. 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.

  5. 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

What is the difference between a dApp and a regular app?

A dApp runs on a decentralized blockchain network, with smart contracts replacing backend servers. User data and logic are transparent and immutable. Unlike regular apps, dApps require wallet authentication instead of username/password and transactions cost gas fees.

Do I need to run a node to build a dApp?

No. dApps connect to blockchain nodes through services like Infura, Alchemy, or QuickNode. These provide API access to Ethereum and other networks without running your own node. For production, use multiple providers for redundancy.

Can dApps be free for users?

Users always pay gas fees for blockchain transactions. However, you can: sponsor transactions using meta-transactions (EIP-2771), use Layer 2 networks with lower fees, or subsidize gas through a relayer service. Reading data from contracts is free.

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