Skip to content
All essays
WebMarch 22, 202412 min

Building Decentralized Applications (dApps)

Learn to build complete decentralized applications with Web3

Ü
Ümit Uz
Mobile & Full Stack Developer

Decentralized applications (dApps) are applications that run on a blockchain or peer-to-peer network of computers instead of a single computer. Let's learn how to build complete dApps from scratch.

What Makes a dApp?

A dApp has these key characteristics:

  • Open source code
  • Data stored on blockchain
  • Cryptographic tokens for operation
  • Uses blockchain for consensus
  • No central authority controlling it

dApp Architecture

┌─────────────┐
│   Frontend  │ (React/Vue/etc.)
├─────────────┤
│  Web3.js /  │
│  Ethers.js  │
├─────────────┤
│  Provider   │ (MetaMask/WalletConnect)
├─────────────┤
│  Blockchain │ (Ethereum/etc.)
└─────────────┘

Setting Up a dApp Project

Project Structure

my-dapp/
├── contracts/        # Smart contracts
├── src/
│   ├── components/   # React components
│   ├── hooks/        # Custom hooks
│   ├── utils/        # Web3 utilities
│   └── App.jsx
├── public/
│   └── index.html
├── package.json
├── hardhat.config.js
└── .env

Initialize Project

bash
mkdir my-dapp
cd my-dapp
npm init -y

npm install --save-dev hardhat
npm install ethers@5 @openzeppelin/contracts
npx hardhat

Smart Contract Development

Simple Storage Contract

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/Ownable.sol";

contract SimpleStorage is Ownable {
    struct Item {
        uint256 id;
        string name;
        uint256 value;
        address creator;
    }

    mapping(uint256 => Item) public items;
    uint256 public itemCount;

    event ItemCreated(
        uint256 indexed id,
        string name,
        uint256 value,
        address creator
    );

    event ItemUpdated(
        uint256 indexed id,
        string name,
        uint256 value
    );

    function createItem(
        string memory _name,
        uint256 _value
    ) public {
        itemCount++;
        items[itemCount] = Item(
            itemCount,
            _name,
            _value,
            msg.sender
        );
        emit ItemCreated(itemCount, _name, _value, msg.sender);
    }

    function updateItem(
        uint256 _id,
        string memory _name,
        uint256 _value
    ) public {
        require(items[_id].creator == msg.sender, "Not owner");
        items[_id].name = _name;
        items[_id].value = _value;
        emit ItemUpdated(_id, _name, _value);
    }

    function getItem(uint256 _id)
        public
        view
        returns (
            uint256 id,
            string memory name,
            uint256 value,
            address creator
        )
    {
        Item memory item = items[_id];
        return (item.id, item.name, item.value, item.creator);
    }

    function getAllItems()
        public
        view
        returns (Item[] memory)
    {
        Item[] memory allItems = new Item[](itemCount);
        for (uint256 i = 0; i < itemCount; i++) {
            allItems[i] = items[i + 1];
        }
        return allItems;
    }
}

Frontend with React

Install Dependencies

bash
npm install react react-dom
npm install ethers
npm install --save-dev vite @vitejs/plugin-react

Web3 Provider Setup

jsx
// src/Web3Provider.jsx
import { createContext, useContext, useState, useEffect } from 'react';
import { ethers } from 'ethers';

const Web3Context = createContext();

export const useWeb3 = () => {
    const context = useContext(Web3Context);
    if (!context) {
        throw new Error('useWeb3 must be used within Web3Provider');
    }
    return context;
};

export const Web3Provider = ({ children }) => {
    const [account, setAccount] = useState(null);
    const [provider, setProvider] = useState(null);
    const [signer, setSigner] = useState(null);
    const [chainId, setChainId] = useState(null);

    const connectWallet = async () => {
        if (window.ethereum) {
            try {
                const provider = new ethers.providers.Web3Provider(
                    window.ethereum
                );
                await provider.send('eth_requestAccounts', []);
                const signer = provider.getSigner();
                const address = await signer.getAddress();
                const network = await provider.getNetwork();

                setProvider(provider);
                setSigner(signer);
                setAccount(address);
                setChainId(network.chainId);
            } catch (error) {
                console.error('Error connecting wallet:', error);
            }
        } else {
            alert('Please install MetaMask!');
        }
    };

    const disconnectWallet = () => {
        setAccount(null);
        setProvider(null);
        setSigner(null);
        setChainId(null);
    };

    useEffect(() => {
        if (window.ethereum) {
            window.ethereum.on('accountsChanged', (accounts) => {
                if (accounts.length === 0) {
                    disconnectWallet();
                } else {
                    setAccount(accounts[0]);
                }
            });

            window.ethereum.on('chainChanged', () => {
                window.location.reload();
            });
        }
    }, []);

    return (
        <Web3Context.Provider
            value={{
                account,
                provider,
                signer,
                chainId,
                connectWallet,
                disconnectWallet
            }}
        >
            {children}
        </Web3Context.Provider>
    );
};

Contract Interaction Hook

jsx
// src/hooks/useContract.js
import { useState, useEffect } from 'react';
import { useWeb3 } from './useWeb3';

const CONTRACT_ADDRESS = '0x...';
const CONTRACT_ABI = [...];

export const useContract = () => {
    const { signer, provider } = useWeb3();
    const [contract, setContract] = useState(null);

    useEffect(() => {
        if (signer) {
            const contractInstance = new ethers.Contract(
                CONTRACT_ADDRESS,
                CONTRACT_ABI,
                signer
            );
            setContract(contractInstance);
        }
    }, [signer]);

    return contract;
};

Create Item Component

jsx
// src/components/CreateItem.jsx
import { useState } from 'react';
import { useWeb3 } from '../useWeb3';
import { useContract } from '../hooks/useContract';

export const CreateItem = () => {
    const { account } = useWeb3();
    const contract = useContract();
    const [name, setName] = useState('');
    const [value, setValue] = useState('');
    const [loading, setLoading] = useState(false);

    const handleSubmit = async (e) => {
        e.preventDefault();
        if (!contract || !account) {
            alert('Please connect wallet');
            return;
        }

        setLoading(true);
        try {
            const tx = await contract.createItem(name, value);
            await tx.wait();
            alert('Item created!');
            setName('');
            setValue('');
        } catch (error) {
            console.error('Error:', error);
            alert('Error creating item');
        } finally {
            setLoading(false);
        }
    };

    return (
        <form onSubmit={handleSubmit}>
            <input
                type="text"
                value={name}
                onChange={(e) => setName(e.target.value)}
                placeholder="Item name"
            />
            <input
                type="number"
                value={value}
                onChange={(e) => setValue(e.target.value)}
                placeholder="Value"
            />
            <button type="submit" disabled={loading}>
                {loading ? 'Creating...' : 'Create Item'}
            </button>
        </form>
    );
};

Display Items Component

jsx
// src/components/ItemList.jsx
import { useState, useEffect } from 'react';
import { useContract } from '../hooks/useContract';

export const ItemList = () => {
    const contract = useContract();
    const [items, setItems] = useState([]);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        loadItems();
    }, [contract]);

    const loadItems = async () => {
        if (!contract) return;

        try {
            const allItems = await contract.getAllItems();
            setItems(allItems);
        } catch (error) {
            console.error('Error loading items:', error);
        } finally {
            setLoading(false);
        }
    };

    if (loading) return <div>Loading...</div>;

    return (
        <div>
            {items.map((item) => (
                <div key={item.id.toString()}>
                    <h3>{item.name}</h3>
                    <p>Value: {item.value.toString()}</p>
                    <p>Creator: {item.creator}</p>
                </div>
            ))}
        </div>
    );
};

Deploying Your dApp

Deploy Contract

bash
npx hardhat compile
npx hardhat run scripts/deploy.js --network mainnet

Deploy Frontend

bash
npm run build
# Deploy to IPFS, Vercel, Netlify, etc.

Best Practices

  1. 1Always handle connection states
  2. 2Show loading indicators during transactions
  3. 3Handle errors gracefully
  4. 4Validate inputs before transactions
  5. 5Use events for real-time updates
  6. 6Implement gas estimation
  7. 7Support multiple networks
  8. 8Test thoroughly on testnets

Conclusion

Building dApps requires understanding both frontend development and blockchain interaction. With the right tools and patterns, you can create powerful decentralized applications that are transparent, secure, and user-controlled.

Enjoyed this essay? Share it.

Related essays

Next essay
Product Backlog Management: Prioritize for Value