Building decentralized applications (dApps) on the Ethereum blockchain is one of the most in-demand skills in the web3 space. Whether you're aiming to become a blockchain developer or simply want to understand how smart contracts power real-world applications, this step-by-step guide will walk you through creating a full-stack dApp from scratch.
In this comprehensive tutorial, we’ll build a decentralized voting application—a secure, tamper-proof system where users can vote for candidates, and every vote is permanently recorded on the blockchain.
What Is a Blockchain?
To understand how dApps work, let’s start with the foundation: blockchain technology.
Imagine a traditional web app. When you interact with it, your browser communicates with a central server that stores all data in a centralized database. This setup has vulnerabilities: data can be altered, servers can be hacked, and trust is placed entirely in a single entity.
Now, contrast that with a blockchain—a decentralized peer-to-peer network of computers (called nodes) that collectively maintain a shared database. There’s no central authority. Instead, every node stores a copy of the entire ledger, and changes are validated through consensus mechanisms.
In blockchain terms:
- Data is stored in blocks
- Blocks are cryptographically linked together in a chain
- This creates an immutable public ledger
When you write data (like a vote), it's broadcast to the network. Miners or validators process the transaction, secure it with cryptographic hashing, and add it permanently to the chain. Once recorded, it cannot be changed.
For our voting dApp, this ensures:
- Votes are permanent
- No double voting
- Transparent and auditable results
And here's a key point: reading data from the blockchain is free, but writing data costs gas—a small fee paid in Ether (ETH) to compensate network participants for computation and storage.
👉 Discover how blockchain transactions work under the hood
Understanding Smart Contracts
At the heart of every Ethereum dApp lies a smart contract—self-executing code deployed on the blockchain.
Think of it as a digital agreement that automatically enforces rules without intermediaries. In our case, the smart contract will:
- Store candidate information
- Record votes
- Prevent duplicate voting
- Emit events when actions occur
Smart contracts are written in Solidity, a JavaScript-like language designed for Ethereum. They run inside the Ethereum Virtual Machine (EVM) and are immutable once deployed—meaning bugs can’t be patched easily. That’s why testing and security are critical.
Our dApp structure will include:
- A front-end interface (HTML/CSS/JavaScript)
- A Solidity smart contract
- Connection via Web3.js to interact with the Ethereum network
Let’s start building.
What We’ll Be Building
We’re creating a full-stack decentralized voting app with these features:
- List of candidates showing ID, name, and vote count
- Voting form allowing users to select a candidate
- Real-time updates using blockchain events
- Protection against double voting
- Front-end powered by Web3 and MetaMask integration
Users connect their wallet, cast a vote (paying gas), and see results update instantly across all clients.
Setting Up Development Dependencies
Before coding, install these essential tools:
Node.js and NPM
Ensure Node.js and its package manager (NPM) are installed:
node -v
npm -v
Truffle Framework
Truffle is a development suite for Ethereum dApps. Install globally:
npm install -g truffle
It provides:
- Smart contract compilation
- Deployment scripts
- Automated testing
- Built-in development console
Ganache
Download Ganache to run a local Ethereum blockchain. It gives you:
- 10 test accounts preloaded with fake ETH
- Fast block mining for development
- Transaction inspection tools
Launch Ganache before proceeding.
MetaMask
Install the MetaMask browser extension to connect your browser to Ethereum networks. Configure it to use Localhost 7545 (default Ganache port).
👉 Learn how wallets interact with dApps securely
Optional: Solidity Syntax Highlighting
Use editors like VS Code or Sublime Text with Solidity plugins for better code readability.
Step 1: Create and Deploy a Basic Smart Contract
Initialize your project:
mkdir election-dapp
cd election-dapp
truffle unbox pet-shop
The Pet Shop box includes boilerplate code:
contracts/
: Solidity filesmigrations/
: Deployment scriptstest/
: Test casessrc/
: Front-end assets
Create your first contract:
touch contracts/Election.sol
Add this initial code:
pragma solidity ^0.4.2;
contract Election {
// Candidate model
struct Candidate {
uint id;
string name;
uint voteCount;
}
// Store candidates by ID
mapping(uint => Candidate) public candidates;
// Track total candidates
uint public candidatesCount;
// Constructor: runs once on deployment
function Election() public {
addCandidate("Candidate 1");
addCandidate("Candidate 2");
}
function addCandidate(string _name) private {
candidatesCount++;
candidates[candidatesCount] = Candidate(candidatesCount, _name, 0);
}
}
This sets up two candidates and stores them on-chain using a mapping
and struct
.
Next, create a migration file:
touch migrations/2_deploy_contracts.js
Add deployment logic:
var Election = artifacts.require("./Election.sol");
module.exports = function(deployer) {
deployer.deploy(Election);
};
Now deploy:
truffle migrate --reset
Test in Truffle Console:
truffle console
> let instance = await Election.deployed()
> await instance.candidatesCount()
// Returns: 2
Success! Your contract is live on your local blockchain.
Step 2: Add Voting Logic
Now enhance the contract with voting functionality.
Add a voters
mapping to track who has voted:
mapping(address => bool) public voters;
Add the vote
function:
function vote(uint _candidateId) public {
// Prevent double voting
require(!voters[msg.sender]);
// Ensure valid candidate
require(_candidateId > 0 && _candidateId <= candidatesCount);
// Mark voter as having voted
voters[msg.sender] = true;
// Increment vote count
candidates[_candidateId].voteCount++;
}
Key points:
msg.sender
is the current user’s wallet addressrequire()
statements enforce rules and revert transactions if violated- State changes cost gas
Redeploy:
truffle migrate --reset
Step 3: Write Automated Tests
Testing is crucial because smart contracts are immutable.
Create test/election.js
:
contract("Election", function(accounts) {
let electionInstance;
it("initializes with two candidates", async () => {
electionInstance = await Election.deployed();
const count = await electionInstance.candidatesCount();
assert.equal(count, 2);
});
it("allows a user to vote once", async () => {
const receipt = await electionInstance.vote(1, { from: accounts[0] });
// Check event fired
assert.equal(receipt.logs.length, 1);
assert.equal(receipt.logs[0].event, "votedEvent");
// Check voter marked as voted
const hasVoted = await electionInstance.voters(accounts[0]);
assert(hasVoted);
// Try voting again — should fail
try {
await electionInstance.vote(1, { from: accounts[0] });
assert.fail();
} catch (error) {
assert(error.message.includes("revert"));
}
});
});
Run tests:
truffle test
All tests should pass—ensuring correctness before going further.
Step 4: Build the Front End
Replace src/index.html
content with:
<div id="content">
<h1>Election Results</h1>
<table class="table">
<thead>
<tr><th>#</th><th>Name</th><th>Votes</th></tr>
</thead>
<tbody id="candidatesResults"></tbody>
</table>
<form id="votingForm">
<h3>Vote for Candidate</h3>
<select id="candidatesSelect"></select>
<button type="submit">Vote</button>
</form>
</div>
<div id="loader">Loading...</div>
Update src/js/app.js
to:
- Load Web3 provider (MetaMask)
- Fetch and display candidates
- Handle voting submission
Core functions include:
render: function() {
// Fetch account from MetaMask
web3.eth.getCoinbase((err, account) => {
App.account = account;
});
// Load candidates from contract
App.contracts.Election.deployed().then(instance => {
return instance.candidatesCount();
}).then((count) => {
let results = $("#candidatesResults");
results.empty();
for (let i = 1; i <= count; i++) {
instance.candidates(i).then((candidate) => {
results.append(`<tr><td>${candidate[0]}</td><td>${candidate[1]}</td><td>${candidate[2]}</td></tr>`);
});
}
});
},
castVote: function(event) {
event.preventDefault();
const candidateId = $('#candidatesSelect').val();
App.contracts.Election.deployed().then(instance => {
return instance.vote(candidateId, { from: App.account });
}).then((result) => {
// Auto-refresh via event listener (next step)
});
}
Step 5: Use Events for Real-Time Updates
Add an event in Election.sol
:
event votedEvent(uint indexed _candidateId);
Trigger it after voting:
votedEvent(_candidateId);
In app.js
, listen for changes:
listenForEvents: function() {
App.contracts.Election.deployed().then(instance => {
instance.votedEvent({}, { fromBlock: 0 }).watch((error, event) => {
App.render(); // Refresh UI automatically
});
});
}
Now votes update across all connected clients in real time!
Frequently Asked Questions (FAQ)
Q: Can anyone view the votes on the blockchain?
A: Yes—blockchain data is public and transparent. Anyone can verify vote counts and transaction history.
Q: How do I prevent fake votes?
A: Each wallet address can only vote once thanks to the voters
mapping and require()
checks.
Q: Is this dApp scalable for large elections?
A: While great for learning, production systems may need layer-2 scaling solutions due to gas costs.
Q: Can I modify the contract after deployment?
A: No—smart contracts are immutable. You’d need to deploy a new version and migrate state manually.
Q: What happens if I enter an invalid candidate ID?
A: The transaction reverts due to the require()
condition, and no gas is wasted beyond processing fees.
👉 Explore advanced dApp scaling techniques
Core Keywords
ethereum dapp tutorial, build decentralized application, solidity smart contract, blockchain voting system, full stack dapp, truffle framework, ganache local blockchain, web3.js integration
With this foundation, you’re ready to explore more complex dApps involving tokens, DAOs, NFTs, and DeFi protocols. Keep building!