Full-Stack Ethereum Development Guide with React, Ethers.js, Solidity, and Hardhat

·

Building decentralized applications (dApps) on the Ethereum Virtual Machine (EVM) opens doors across multiple blockchains, including Ethereum, Polygon, Avalanche, and Celo. This guide walks you through a modern, full-stack web3 development workflow using React, Ethers.js, Solidity, and Hardhat—a powerful combination for creating scalable, interoperable blockchain applications.

Whether you're new to smart contracts or expanding your web3 skillset, this tutorial provides a clear path from setting up your environment to deploying and interacting with live contracts on testnets.


Core Technologies in the Web3 Stack

To build a full-stack dApp, we’ll use five foundational technologies:

1. Hardhat – Ethereum Development Environment

Hardhat is a robust framework designed for compiling, testing, debugging, and deploying Solidity smart contracts. It includes a built-in local Ethereum network for rapid prototyping.

Other tools like Truffle and Foundry exist, but Hardhat stands out for its seamless integration with Ethers.js, TypeScript support, and extensible plugin ecosystem.

👉 Discover how developers are accelerating blockchain projects using modern tooling.

2. Ethers.js – Ethereum Web Client Library

Ethers.js is a lightweight library that enables interaction between frontend apps and the Ethereum blockchain. It simplifies tasks like sending transactions, reading contract data, and managing wallets.

Compared to Web3.js, Ethers.js offers a cleaner API, smaller bundle size, and better security practices—making it ideal for React-based dApps.

3. MetaMask – Wallet Integration

MetaMask acts as the bridge between users and the blockchain. Once connected, it exposes the window.ethereum provider, allowing your app to request account access and sign transactions securely.

No private keys are ever shared; users retain full control over their assets.

4. React – Frontend Framework

React powers dynamic user interfaces with reusable components. Its rich ecosystem (Next.js, Gatsby, etc.) supports server-side rendering, static generation, and real-time updates—perfect for responsive dApp experiences.

5. The Graph (Future Use) – Decentralized Data Querying

While not implemented here, The Graph plays a crucial role in production dApps by indexing blockchain data into GraphQL APIs. This eliminates slow, direct chain queries and enables features like pagination and full-text search.


What We’ll Build

In this tutorial, you’ll create two core smart contracts and connect them to a React frontend:

  1. Greeter Contract: Stores and updates a message on-chain.
  2. Token Contract: Mints a custom token and allows transfers between addresses.

Your React app will allow users to:


Prerequisites

Before starting:

No real ETH is needed—we’ll use testnets with free tokens.


Step 1: Set Up the Project

Start by creating a new React app:

npx create-react-app react-dapp
cd react-dapp

Install required dependencies:

npm install ethers hardhat @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers

Initialize Hardhat:

npx hardhat
? What do you want to do? Create a sample project

This generates key folders: contracts, scripts, test, and hardhat.config.js.

Update hardhat.config.js to fix MetaMask compatibility and set artifact output to the React source directory:

module.exports = {
  solidity: "0.8.4",
  paths: {
    artifacts: './src/artifacts',
  },
  networks: {
    hardhat: {
      chainId: 1337
    }
  }
};

Step 2: Write and Deploy the Greeter Smart Contract

Open contracts/Greeter.sol. This simple contract stores a string and allows reading/updating:

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "hardhat/console.sol";

contract Greeter {
    string greeting;

    constructor(string memory _greeting) {
        console.log("Deploying Greeter with:", _greeting);
        greeting = _greeting;
    }

    function greet() public view returns (string memory) {
        return greeting;
    }

    function setGreeting(string memory _greeting) public {
        console.log("Updating greeting from '%s' to '%s'", greeting, _greeting);
        greeting = _greeting;
    }
}

Compile the Contract

Run:

npx hardhat compile

This generates an ABI (Application Binary Interface) in src/artifacts/contracts/Greeter.sol/Greeter.json. The ABI defines how your frontend interacts with the contract.


Step 3: Deploy to Local Network

Start a local blockchain:

npx hardhat node

This creates 20 test accounts with 10,000 ETH each.

Rename scripts/sample-script.js to scripts/deploy.js and update it:

const hre = require("hardhat");

async function main() {
  const Greeter = await hre.ethers.getContractFactory("Greeter");
  const greeter = await Greeter.deploy("Hello, World!");
  await greeter.deployed();
  console.log("Greeter deployed to:", greeter.address);
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});

Deploy:

npx hardhat run scripts/deploy.js --network localhost

Note the contract address—you'll need it in the frontend.

👉 See how top developers streamline deployment workflows across chains.


Step 4: Connect React Frontend

Update src/App.js:

import './App.css';
import { useState } from 'react';
import { ethers } from 'ethers';
import Greeter from './artifacts/contracts/Greeter.sol/Greeter.json';

const greeterAddress = "YOUR_CONTRACT_ADDRESS"; // Replace

function App() {
  const [greeting, setGreetingValue] = useState();

  async function requestAccount() {
    await window.ethereum.request({ method: 'eth_requestAccounts' });
  }

  async function fetchGreeting() {
    if (typeof window.ethereum !== 'undefined') {
      const provider = new ethers.providers.Web3Provider(window.ethereum);
      const contract = new ethers.Contract(greeterAddress, Greeter.abi, provider);
      const data = await contract.greet();
      console.log('Greeting:', data);
    }
  }

  async function setGreeting() {
    if (!greeting) return;
    await requestAccount();
    const provider = new ethers.providers.Web3Provider(window.ethereum);
    const signer = provider.getSigner();
    const contract = new ethers.Contract(greeterAddress, Greeter.abi, signer);
    const transaction = await contract.setGreeting(greeting);
    await transaction.wait();
    fetchGreeting();
  }

  return (
    <div>
      <button onClick={fetchGreeting}>Fetch Greeting</button>
      <button onClick={setGreeting}>Set Greeting</button>
      <input onChange={e => setGreetingValue(e.target.value)} placeholder="Set greeting" />
    </div>
  );
}

export default App;

Start the app:

npm start

Connect MetaMask to localhost:8545, import one of the test accounts using its private key, and interact with the contract.


Step 5: Deploy to Ropsten Testnet

To make your contract publicly accessible:

  1. Switch MetaMask to Ropsten Test Network
  2. Get test ETH from a faucet (e.g., https://faucet.ropsten.be/)
  3. Create an Infura or Alchemy project for RPC access

Update hardhat.config.js:

ropsten: {
  url: "https://ropsten.infura.io/v3/YOUR_PROJECT_ID",
  accounts: [`0x${YOUR_PRIVATE_KEY}`]
}

Deploy:

npx hardhat run scripts/deploy.js --network ropsten

Verify deployment on Ropsten Etherscan.


Step 6: Build a Custom Token Contract

Create contracts/Token.sol:

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

contract Token {
    string public name = "Nader Dabit Token";
    string public symbol = "NDT";
    uint public totalSupply = 1000000;
    mapping(address => uint) balances;

    constructor() {
        balances[msg.sender] = totalSupply;
    }

    function transfer(address to, uint amount) external {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        balances[msg.sender] -= amount;
        balances[to] += amount;
    }

    function balanceOf(address account) external view returns (uint) {
        return balances[account];
    }
}

Update scripts/deploy.js to deploy both contracts.

Update frontend to include balance check and transfer functionality.


Step 7: Upgrade to ERC20 Standard

For interoperability, use OpenZeppelin’s ERC20 implementation:

npm install @openzeppelin/contracts

Create NDToken.sol:

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract NDToken is ERC20 {
    constructor() ERC20("Nader Dabit Token", "NDT") {
        _mint(msg.sender, 100000 * (10 ** 18));
    }
}

This inherits standard functions like transfer, balanceOf, and approve.


Frequently Asked Questions

Q1: What is the difference between reading and writing to a blockchain?

Reading data (e.g., view functions) is free. Writing (e.g., state changes) requires gas fees paid in ETH.

Q2: Why use Hardhat over Truffle?

Hardhat offers better TypeScript support, built-in testing network, and superior debugging tools like console logs in Solidity.

Q3: Can I connect wallets other than MetaMask?

Yes—use Web3Modal to support WalletConnect, Coinbase Wallet, and more with minimal configuration.

Q4: Is Ethers.js better than Web3.js?

Ethers.js is more secure, modular, and has a smaller footprint—ideal for modern dApps.

Q5: How do I handle private keys securely?

Never hardcode keys. Use environment variables or wallet signers via MetaMask.

Q6: What’s next after building basic dApps?

Explore The Graph for indexing data, IPFS for decentralized storage, and Layer 2 solutions for scalability.


Final Thoughts

This guide covers the essential stack for full-stack Ethereum development in 2025: React, Ethers.js, Solidity, and Hardhat. You’ve learned how to write, compile, deploy, and interact with smart contracts—both locally and on testnets.

As web3 evolves, mastering these tools ensures you can build secure, user-friendly dApps across EVM-compatible chains.

👉 Accelerate your blockchain journey with tools trusted by developers worldwide.