Create a Simple dApp: A Step-by-Step Guide for Web3 Developers

·

Building decentralized applications (dApps) is a foundational skill for developers entering the Web3 ecosystem. This guide walks you through creating a simple JavaScript dApp and integrating it with MetaMask—the most widely used Ethereum wallet. You'll learn how to detect the MetaMask provider, identify the user’s network, and securely access their accounts using modern Web3 practices.

Whether you're new to blockchain development or expanding your full-stack skills, this hands-on tutorial provides a clear entry point into dApp development while emphasizing best practices for real-world deployment.

👉 Discover how to build and deploy your first blockchain application with confidence.


Prerequisites

Before diving into the code, ensure your development environment meets the following requirements:

These tools will support smooth project setup and local testing. Once everything is in place, you're ready to begin building.


Step 1: Set Up Your Development Project

Start by initializing a new project using Vite, a fast front-end build tool that enhances developer experience with instant hot module replacement.

Run the following command in your terminal:

npm create vite@latest simple-dapp -- --template vanilla

This creates a minimal JavaScript project with HTML, CSS, and vanilla JS—perfect for learning dApp fundamentals without framework complexity.

Navigate into the project directory:

cd simple-dapp

Then install dependencies:

npm install

Now you have a clean, modern development environment ready for Web3 integration.


Step 2: Build the Basic dApp Structure

Create a main.js file inside the src folder (or root if not using a dedicated source directory):

import "./style.css";

document.querySelector("#app").innerHTML = `
  <button class="enableEthereumButton">Enable Ethereum</button>
  <div class="showAccount"></div>
`;

Next, update the index.html file to include your script and set up the page structure:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Simple dApp</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

This basic UI includes a button to trigger MetaMask connection and a placeholder to display the connected Ethereum account.


Step 3: Detect the MetaMask Provider

To interact with MetaMask, your dApp must first detect whether the wallet’s provider is available in the browser.

Note: While this tutorial uses window.ethereum for simplicity, production apps should use EIP-6963 for wallet interoperability.

Install the legacy detection utility (for educational use only):

npm i @metamask/detect-provider

Create a src/detect.js file and add the following:

import detectEthereumProvider from "@metamask/detect-provider";

async function setup() {
  const provider = await detectEthereumProvider();

  if (provider && provider === window.ethereum) {
    console.log("MetaMask is available!");
    startApp(provider);
  } else {
    console.log("Please install MetaMask!");
  }
}

function startApp(provider) {
  if (provider !== window.ethereum) {
    console.error("Do you have multiple wallets installed?");
  }
}

window.addEventListener("load", setup);

This script safely checks for MetaMask on page load and initializes your app only if detected.

👉 Learn how real-time blockchain interactions power next-gen dApps.


Step 4: Detect and Respond to Network Changes

Your dApp should always know which Ethereum network the user is connected to—Mainnet, Sepolia, or a local testnet—so it can send transactions correctly.

Update detect.js with network detection logic:

const chainId = await window.ethereum.request({ method: "eth_chainId" });
console.log("Connected to chain:", chainId);

window.ethereum.on("chainChanged", handleChainChanged);

function handleChainChanged(chainId) {
  // Best practice: reload to avoid inconsistent state
  window.location.reload();
}

This listens for network switches (e.g., from Ethereum to Polygon) and triggers a reload to keep your app in sync.

Using eth_chainId ensures your dApp avoids errors caused by mismatched network assumptions.


Step 5: Access User Accounts Securely

To perform actions like sending transactions or reading balances, your dApp must request permission to access the user’s Ethereum accounts.

Add account access functionality to detect.js:

const ethereumButton = document.querySelector(".enableEthereumButton");
const showAccount = document.querySelector(".showAccount");

ethereumButton.addEventListener("click", () => {
  getAccount();
});

async function getAccount() {
  const accounts = await window.ethereum
    .request({ method: "eth_requestAccounts" })
    .catch((err) => {
      if (err.code === 4001) {
        console.log("User rejected the request to connect.");
      } else {
        console.error(err);
      }
    });

  if (accounts) {
    const account = accounts[0];
    showAccount.innerHTML = `Connected: ${account}`;
  }
}

Ensure this request is triggered only by user action (like clicking a button) to avoid spamming or being blocked by MetaMask.

Update index.html's script tag to load detect.js:

<script type="module" src="/src/detect.js"></script>

Now, when users click "Enable Ethereum," MetaMask prompts them to approve the connection—keeping control in their hands.


Final Code Overview

Here’s the complete detect.js file combining all components:

import detectEthereumProvider from "@metamask/detect-provider";

// Detect provider
async function setup() {
  const provider = await detectEthereumProvider();
  if (provider && provider === window.ethereum) {
    console.log("MetaMask detected");
    startApp(provider);
  } else {
    console.log("Install MetaMask");
  }
}

function startApp(provider) {
  if (provider !== window.ethereum) {
    console.error("Multiple wallets detected?");
  }
}

// Network detection
async function handleChainChanged(chainId) {
  window.location.reload();
}

// Account access
async function getAccount() {
  const accounts = await window.ethereum.request({ method: "eth_requestAccounts" });
  showAccount.innerHTML = accounts[0];
}

const ethereumButton = document.querySelector(".enableEthereumButton");
const showAccount = document.querySelector(".showAccount");

ethereumButton.addEventListener("click", getAccount);

window.ethereum.on("chainChanged", handleChainChanged);
window.addEventListener("load", setup);

And the final index.html:

<div id="app"></div>
<script type="module" src="/src/detect.js"></script>

Run npm run dev to launch your local server and test the dApp in-browser.


Frequently Asked Questions

Why should I use EIP-6963 instead of window.ethereum?

EIP-6963 enables wallet interoperability, allowing your dApp to detect and support multiple wallets (like MetaMask, WalletConnect, etc.) simultaneously. It provides a standardized way to discover all available injected wallets without relying solely on window.ethereum.

Is @metamask/detect-provider safe for production?

No. The @metamask/detect-provider package is deprecated and intended only for educational purposes. In production, use EIP-6963-compliant detection methods to future-proof your application.

What does error code 4001 mean?

Error code 4001 indicates that the user rejected the connection request. It's part of EIP-1193 and is not an error condition—it simply means the user chose not to connect. Handle it gracefully with a friendly message.

How do I test my dApp on different networks?

Use MetaMask’s network selector to switch between Ethereum Mainnet, testnets (like Sepolia), or custom RPCs. Your dApp should respond to chainChanged events and adjust behavior accordingly.

Can I store user account data permanently?

No. Never assume persistent access. Always re-request accounts on page load or after network changes. Users may disconnect or switch accounts at any time.

What are the next steps after building this simple dApp?

Explore building a React-based dApp with local state management, integrating TypeScript, and connecting to smart contracts using libraries like ethers.js or viem.

👉 Explore advanced tools and APIs that accelerate Web3 development.


Core Keywords

By mastering these fundamentals, you lay the groundwork for building secure, user-friendly decentralized applications that interact seamlessly with Ethereum and beyond.