Introduction
In 2026, Hardhat remains the gold standard for developing, testing, and deploying Ethereum smart contracts. Unlike the more rigid Truffle, Hardhat offers unmatched flexibility with its plugins, built-in REPL, and native chain forking support. This intermediate tutorial walks you through creating a full project step by step: from installation to deploying on Sepolia (Ethereum testnet).
Why Hardhat? It speeds up debugging with detailed traces, integrates Mocha/Chai for robust tests, and handles multi-network deployments via Ethers.js. Think of it like a mechanic's workshop: all the tools at hand to assemble a Solidity engine without friction. By the end, you'll have a working Greeter contract that's tested and deployed. Ready to dive into Web3? (142 words)
Prerequisites
- Node.js 20+ and npm/yarn/pnpm
- Basic Solidity knowledge (variables, functions, events)
- MetaMask account with Sepolia ETH (from a faucet)
- Git for version control
- Editor like VS Code with Solidity extension
Initialize the Hardhat Project
mkdir hardhat-greeter && cd hardhat-greeter
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox @nomicfoundation/hardhat-verify
npx hardhat init
# Select 'Create a JavaScript project' then 'Yes, with a sample project'These commands create a project folder, initialize package.json, and install Hardhat with its toolbox (Solidity compiler, tests, ethers.js, verification). The init generates a boilerplate structure: contracts/, scripts/, test/, hardhat.config.js. Avoid global Hardhat versions to isolate dependencies.
Understanding the Generated Structure
After init, your project includes:
- contracts/: Solidity smart contracts.
- scripts/: JS scripts for deployment.
- test/: Unit tests with Mocha/Chai.
- hardhat.config.js: Central config (networks, Solidity version).
It's like a prefab house: ready to customize. We'll replace the sample with a custom Greeter.
Configure hardhat.config.js
require('@nomicfoundation/hardhat-toolbox');
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: {
version: '0.8.28',
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
},
networks: {
hardhat: {},
sepolia: {
url: 'https://sepolia.infura.io/v3/VOTRE_CLE_INFURA',
accounts: ['0xVOTRE_PRIVATE_KEY']
}
},
etherscan: {
apiKey: {
sepolia: 'VOTRE_CLE_ETHERSCAN'
}
}
};This config sets Solidity 0.8.28 with optimization (reduces gas costs). Adds Sepolia via Infura (replace with your keys). The 'hardhat' network simulates a local blockchain for testing. Swap placeholders for real keys (free Infura/Etherscan). Pitfall: Forgetting the optimizer hikes production gas costs.
Create the Greeter Smart Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
contract Greeter {
string private _greeting;
constructor(string memory greeting) {
_greeting = greeting;
}
function greet() public view returns (string memory) {
return _greeting;
}
function setGreeting(string memory greeting) public {
_greeting = greeting;
}
}This contract stores a private message, exposes a 'greet()' getter, and a modifiable setter. Uses ^0.8.28 for security (built-in SafeMath). Constructor initializes it. Real-world example: Deploy with 'Hello Hardhat!' and update to 'Hello 2026'. Copy-paste directly.
Compile the Contract
Hardhat auto-compiles .sol files into artifacts (ABI + bytecode). Run npx hardhat compile to verify. Output goes to artifacts/ and cache/. Analogy: Like a C compiler to machine code, but for the EVM.
Write Unit Tests
const { expect } = require('chai');
const { ethers } = require('hardhat');
describe('Greeter', function () {
it('Should return the greeting provided in constructor', async function () {
const Greeter = await ethers.getContractFactory('Greeter');
const greeter = await Greeter.deploy('Hello Hardhat!');
await greeter.waitForDeployment();
expect(await greeter.greet()).to.equal('Hello Hardhat!');
});
it('Should emit event on setGreeting', async function () {
const Greeter = await ethers.getContractFactory('Greeter');
const greeter = await Greeter.deploy('Hello Hardhat!');
await greeter.waitForDeployment();
await expect(greeter.setGreeting('Hola 2026'))
.to.emit(greeter, 'GreetingUpdated') // Add event if needed
.to.not.be.reverted;
});
});Tests verify the constructor and setter using Chai expect. Uses ethers for factory/deploy. Second test checks non-revert. Run with npx hardhat test. Add a GreetingUpdated event for full coverage. Pitfall: Skipping waitForDeployment() causes timeouts.
Deployment Script
const hre = require('hardhat');
async function main() {
const Greeter = await hre.ethers.getContractFactory('Greeter');
const greeter = await Greeter.deploy('Hello Hardhat 2026!');
await greeter.waitForDeployment();
console.log('Greeter deployed to:', await greeter.getAddress());
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});Script deploys via ethers factory and logs the address. hre provides Hardhat Runtime Environment access. Copy to scripts/, run npx hardhat run scripts/deploy.js --network hardhat. For Sepolia: --network sepolia. Error handling with catch.
Deploy and Verify
Local: npx hardhat run scripts/deploy.js
Sepolia: npx hardhat run scripts/deploy.js --network sepolia
Verify: npx hardhat verify --network sepolia CONTRACT_ADDRESS "Hello Hardhat 2026!"
Console shows the address. Etherscan verification makes the contract public (source code).
Fork a Chain for Advanced Testing
networks: {
hardhat: {
forking: {
url: 'https://eth-mainnet.g.alchemy.com/v2/VOTRE_CLE_ALCHEMY',
blockNumber: 20000000
}
}
},Add this to hardhat.config.js to fork Mainnet (free Alchemy). Test on real state without gas costs. Example: Impersonate a whale for massive transfers. Restart npx hardhat node for REPL. Pitfall: Use a recent blockNumber to avoid stale forks.
Best Practices
- Optimize Solidity: Enable optimizer (runs:200+) for <10% gas savings.
- Test Coverage: Add
npm i -D hardhat-gas-reporter solidity-coverageand integrate in config. - Secure Envs: Use
dotenvfor keys (.env+require('dotenv').config()). - Gas Reporting: Add gasReporter to hardhat.config.js for benchmarks.
- Multi-Compiler: Support 0.8.x + legacy 0.5.x if needed.
Common Errors to Avoid
- Exposed Private Key: Never hardcode in config; use
process.env.PRIVATE_KEY. - Solidity Version Mismatch: pragma ^0.8.28 needs config 0.8.28 or compile fails.
- No waitForDeployment(): Async tests fail without it (post-London fork).
- Unfunded Network: Check Sepolia faucet; test local first.
Next Steps
- Official Docs: Hardhat Book
- Advanced Plugins: Foundry for faster tests, Tenderly for debugging.
- Training: Check out our Ethereum courses at Learni
- Example Repo: Fork this tutorial on GitHub to experiment.