Choosing a Web3 Library
To interact with the Ethereum blockchain from JavaScript or TypeScript, you need a library that handles JSON-RPC communication, ABI encoding/decoding, and wallet integration. The two dominant libraries are ethers.js (lightweight, modern, TypeScript-first) and web3.js (the original Ethereum JS library, recently rewritten for v4). In modern development, ethers.js v6 is the most popular choice for new projects.
Library Comparison
| Feature | ethers.js v6 | web3.js v4 |
|---|---|---|
| Bundle Size | ~120KB (tree-shakeable) | ~400KB |
| TypeScript | Native, first-class | Full support in v4 |
| Provider/Signer | Separated cleanly | Combined in Web3 instance |
| License | MIT | LGPL-3.0 |
| ENS Support | Built-in | Plugin-based |
Setting Up ethers.js
// Install: npm install ethers
import { ethers } from 'ethers';
// --- Providers (read-only connection to the blockchain) ---
// Connect to a public RPC endpoint
const jsonProvider = new ethers.JsonRpcProvider('https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY');
// Connect to the user's browser wallet (MetaMask, etc.)
const browserProvider = new ethers.BrowserProvider(window.ethereum);
// Read blockchain data
const blockNumber = await jsonProvider.getBlockNumber();
const balance = await jsonProvider.getBalance('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045');
console.log('Block:', blockNumber);
console.log('Balance:', ethers.formatEther(balance), 'ETH');
// --- Signers (can sign transactions) ---
const signer = await browserProvider.getSigner();
const myAddress = await signer.getAddress();
console.log('Connected wallet:', myAddress);
// Send ETH
const tx = await signer.sendTransaction({
to: '0xRecipientAddress...',
value: ethers.parseEther('0.1'),
});
const receipt = await tx.wait();
console.log('Confirmed in block:', receipt.blockNumber);
// --- Working with contracts ---
const contractABI = [
'function balanceOf(address owner) view returns (uint256)',
'function transfer(address to, uint256 amount) returns (bool)',
'event Transfer(address indexed from, address indexed to, uint256 amount)',
];
// Read-only contract (with provider)
const tokenRead = new ethers.Contract(tokenAddress, contractABI, jsonProvider);
const balance2 = await tokenRead.balanceOf(myAddress);
console.log('Token balance:', ethers.formatUnits(balance2, 18));
// Read-write contract (with signer)
const tokenWrite = new ethers.Contract(tokenAddress, contractABI, signer);
const transferTx = await tokenWrite.transfer(recipientAddress, ethers.parseUnits('100', 18));
await transferTx.wait();
Working with ABIs
The Application Binary Interface (ABI) defines how to encode function calls and decode return values for a smart contract. It is a JSON array describing the contract's functions, events, and errors. You get the ABI from the Solidity compiler output or from tools like Hardhat.
// Full ABI format (from compiler output)
const fullABI = [
{
"type": "function",
"name": "transfer",
"inputs": [
{ "name": "to", "type": "address" },
{ "name": "amount", "type": "uint256" }
],
"outputs": [{ "name": "", "type": "bool" }],
"stateMutability": "nonpayable"
},
{
"type": "event",
"name": "Transfer",
"inputs": [
{ "name": "from", "type": "address", "indexed": true },
{ "name": "to", "type": "address", "indexed": true },
{ "name": "amount", "type": "uint256", "indexed": false }
]
}
];
// Human-readable ABI (ethers.js exclusive feature)
const humanABI = [
'function transfer(address to, uint256 amount) returns (bool)',
'function approve(address spender, uint256 amount) returns (bool)',
'function balanceOf(address) view returns (uint256)',
'function totalSupply() view returns (uint256)',
'event Transfer(address indexed from, address indexed to, uint256 amount)',
'event Approval(address indexed owner, address indexed spender, uint256 amount)',
];
// Both formats work identically with ethers.js
const contract = new ethers.Contract(address, humanABI, provider);
Listening to Events
import { ethers } from 'ethers';
const provider = new ethers.JsonRpcProvider(rpcUrl);
const contract = new ethers.Contract(tokenAddress, abi, provider);
// Listen for real-time events
contract.on('Transfer', (from, to, amount, event) => {
console.log(`Transfer: ${from} -> ${to}: ${ethers.formatUnits(amount, 18)} tokens`);
console.log('Transaction hash:', event.log.transactionHash);
});
// Query historical events
const filter = contract.filters.Transfer(null, myAddress); // Transfers TO me
const events = await contract.queryFilter(filter, -10000); // Last 10,000 blocks
for (const event of events) {
const { from, to, amount } = event.args;
console.log(`Received ${ethers.formatUnits(amount, 18)} from ${from}`);
}
// Remove listener when done
contract.removeAllListeners('Transfer');
Using web3.js v4
// Install: npm install web3
import { Web3 } from 'web3';
// Connect to provider
const web3 = new Web3('https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY');
// Or use browser wallet
const web3Browser = new Web3(window.ethereum);
// Read blockchain data
const blockNumber = await web3.eth.getBlockNumber();
const balance = await web3.eth.getBalance('0xAddress...');
console.log('Balance:', web3.utils.fromWei(balance, 'ether'), 'ETH');
// Create contract instance
const contract = new web3.eth.Contract(abi, contractAddress);
// Call view function (free, no transaction)
const totalSupply = await contract.methods.totalSupply().call();
// Send transaction (requires account)
const accounts = await web3.eth.getAccounts();
const tx = await contract.methods.transfer(recipient, amount).send({
from: accounts[0],
gas: '100000',
});
console.log('TX hash:', tx.transactionHash);
// Subscribe to events
const subscription = contract.events.Transfer({
filter: { to: myAddress },
});
subscription.on('data', (event) => {
console.log('Transfer received:', event.returnValues);
});
subscription.on('error', (error) => {
console.error('Event error:', error);
});
Utility Functions
import { ethers } from 'ethers';
// Unit conversion
ethers.parseEther('1.5'); // 1500000000000000000n (Wei)
ethers.formatEther(1500000000000000000n); // "1.5"
ethers.parseUnits('100', 6); // 100000000n (USDC has 6 decimals)
ethers.formatUnits(100000000n, 6); // "100.0"
// Address utilities
ethers.isAddress('0x1234...'); // Validate address format
ethers.getAddress('0xabc...'); // Checksum an address
// Hashing
ethers.keccak256(ethers.toUtf8Bytes('hello')); // Keccak-256 hash
ethers.solidityPackedKeccak256(['uint256', 'address'], [42, addr]);
// ABI encoding
const coder = ethers.AbiCoder.defaultAbiCoder();
const encoded = coder.encode(['uint256', 'string'], [42, 'hello']);
const decoded = coder.decode(['uint256', 'string'], encoded);
// Signing messages
const signer = await provider.getSigner();
const message = 'Sign in to MyDApp at 2026-04-06T12:00:00Z';
const signature = await signer.signMessage(message);
// Verify signature
const recoveredAddress = ethers.verifyMessage(message, signature);
console.log('Signed by:', recoveredAddress);
Best Practices
- Always use BigInt: Ethereum values overflow JavaScript numbers. Use native BigInt or ethers.js BigNumber.
- Handle errors gracefully: Wrap blockchain calls in try/catch. RPC calls can fail, transactions can revert.
- Wait for confirmations: A single confirmation may not be final. Wait for multiple blocks for high-value transactions.
- Use environment variables: Never hardcode API keys or private keys in your source code.
- Test on testnets first: Always deploy and test on Sepolia before going to mainnet.