Web3

Solidity for Web Developers: Build Your First Smart Contract

Tech Setup1 min read
TS

Tech Setup

Published July 31, 2026 · Editorial policy

Solidity for Web Developers: Build Your First Smart Contract

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

LocationWhereCostUse Case
storageOn-chainExpensiveState variables
memoryIn functionCheapFunction parameters
calldataIn functionFreeRead-only input

Visibility

KeywordMeaning
publicAnyone can call
privateOnly this contract
internalThis contract + derived
externalOnly 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

  1. Use uint256 — EVM operates on 256-bit words
  2. Pack storage — declare smaller types together
  3. Cache storage in memory — read once, use multiple times
  4. Use events for data you don't need to read on-chain
  5. Avoid loops with unbounded size
  6. Use calldata instead of memory for 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)