Solidity for Web Developers: Build Your First Smart Contract
Tech Setup1 min read
TS
Published July 31, 2026 · Editorial policy

What is Solidity?
Solidity is the primary language for writing smart contracts on Ethereum and EVM-compatible chains (Polygon, BSC, Arbitrum). It's statically typed and syntax-heavy — think TypeScript meets C++.
Environment Setup
Install Node.js and Hardhat
mkdir solidity-101 && cd solidity-101
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npx hardhat
Select "Create a JavaScript project" and accept defaults.
Project Structure
solidity-101/
├── contracts/
│ └── Greeter.sol
├── test/
│ └── Greeter.test.js
├── scripts/
│ └── deploy.js
├── hardhat.config.js
└── package.json
Your First Contract
contracts/Greeter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Greeter {
string private greeting;
address public owner;
uint256 public greetingCount;
event GreetingChanged(string oldGreeting, string newGreeting, uint256 count);
constructor(string memory _greeting) {
greeting = _greeting;
owner = msg.sender;
greetingCount = 0;
}
function greet() public view returns (string memory) {
return greeting;
}
function setGreeting(string memory _greeting) public {
require(msg.sender == owner, "Only owner can change greeting");
require(bytes(_greeting).length > 0, "Greeting cannot be empty");
string memory old = greeting;
greeting = _greeting;
greetingCount++;
emit GreetingChanged(old, _greeting, greetingCount);
}
function getGreetingCount() public view returns (uint256) {
return greetingCount;
}
receive() external payable {}
}
Key Concepts
Data Locations
| Location | Where | Cost | Use Case |
|---|---|---|---|
storage | On-chain | Expensive | State variables |
memory | In function | Cheap | Function parameters |
calldata | In function | Free | Read-only input |
Visibility
| Keyword | Meaning |
|---|---|
public | Anyone can call |
private | Only this contract |
internal | This contract + derived |
external | Only from outside |
Modifiers
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function withdraw() public onlyOwner {
// only owner can call
}
Testing with Hardhat
test/Greeter.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Greeter", function () {
let greeter, owner, addr1;
beforeEach(async function () {
[owner, addr1] = await ethers.getSigners();
const Greeter = await ethers.getContractFactory("Greeter");
greeter = await Greeter.deploy("Hello, World!");
});
it("should return initial greeting", async function () {
expect(await greeter.greet()).to.equal("Hello, World!");
});
it("should allow owner to change greeting", async function () {
await greeter.setGreeting("Hola!");
expect(await greeter.greet()).to.equal("Hola!");
});
it("should emit event on change", async function () {
await expect(greeter.setGreeting("Hi!"))
.to.emit(greeter, "GreetingChanged")
.withArgs("Hello, World!", "Hi!", 1);
});
it("should reject non-owner", async function () {
await expect(
greeter.connect(addr1).setGreeting("Hacked!")
).to.be.revertedWith("Only owner can change greeting");
});
it("should track greeting count", async function () {
await greeter.setGreeting("First");
await greeter.setGreeting("Second");
expect(await greeter.getGreetingCount()).to.equal(2);
});
});
Run Tests
npx hardhat test
Deployment
scripts/deploy.js
async function main() {
const [deployer] = await ethers.getSigners();
console.log("Deploying with:", deployer.address);
const Greeter = await ethers.getContractFactory("Greeter");
const greeter = await Greeter.deploy("Hello, Hardhat!");
await greeter.waitForDeployment();
console.log("Greeter deployed to:", await greeter.getAddress());
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Deploy to Testnet (Sepolia)
# hardhat.config.js
module.exports = {
networks: {
sepolia: {
url: "https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY",
accounts: [process.env.PRIVATE_KEY],
},
},
};
# Deploy
npx hardhat run scripts/deploy.js --network sepolia
Verify on Etherscan
npx hardhat verify --network sepolia CONTRACT_ADDRESS "Hello, Hardhat!"
Gas Optimization Tips
- Use
uint256— EVM operates on 256-bit words - Pack storage — declare smaller types together
- Cache storage in memory — read once, use multiple times
- Use events for data you don't need to read on-chain
- Avoid loops with unbounded size
- Use
calldatainstead ofmemoryfor read-only params
Common Patterns
Ownable
abstract contract Ownable {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
}
Pausable
bool public paused;
modifier whenNotPaused() {
require(!paused, "Contract is paused");
_;
}
function pause() public onlyOwner {
paused = true;
}
function unpause() public onlyOwner {
paused = false;
}
Resources
- Solidity Docs: docs.soliditylang.org
- OpenZeppelin Contracts: github.com/OpenZeppelin/openzeppelin-contracts
- Etherscan: sepolia.etherscan.io (testnet explorer)
- Remix IDE: remix.ethereum.org (browser-based IDE)


