In 2021 I took on a project that required writing Solidity smart contracts. I had zero blockchain experience. My mental model was: "It's just code that runs on a distributed database." That mental model is wrong in ways that took me months to unlearn.
This is the guide I wish I had for developers who know JavaScript or TypeScript, understand databases and APIs, and are about to write their first production smart contract.
The Fundamental Mental Shift
In web development, mistakes are cheap. You deploy a broken API, you fix it, you deploy again. The broken version might have caused a few errors in your logs. In smart contract development, mistakes are permanent and potentially expensive. Once a contract is deployed to mainnet, that code runs forever exactly as written. If you have a bug, you deploy a *new* contract and migrate to it which costs gas and requires users to update any references they hold.
The DAO hack in 2016 drained $60 million in ETH from a reentrancy bug. The Poly Network hack in 2021 stole $611 million from a single function visibility mistake. These are not theoretical risks. Write every contract assuming an adversary will probe every code path.
The other shift is economic. Every operation in a smart contract costs gas a fee paid by the caller in ETH. Reading from storage is cheaper than writing. Writing a new storage slot costs 20,000 gas. Simple arithmetic is 3 gas. This means you care about computational complexity in a way you never did in traditional backend development.
Solidity for TypeScript Developers
The surface syntax will feel familiar. The semantics are completely different.
Key Differences from TypeScript
- No floats. Solidity has no floating-point numbers. Use integers scaled by a factor (e.g. represent $1.50 as
150with 2 decimal places, or use1.5 * 10^18for 18-decimal token math). - Integer overflow protection is built in since 0.8.0. Before 0.8,
uint8(255) + 1would silently wrap to 0. Now it reverts. This eliminated an entire class of bugs. - `msg.sender` is the address that called this function. Not necessarily the contract owner. This is the foundation of access control.
- `require(condition, message)` is your validation. If condition is false, the entire transaction reverts and gas used so far is lost (but the state change doesn't persist).
- There is no `null`. Unset mappings return 0. Unset addresses return
address(0). Unset booleans returnfalse. Always check for zero-address explicitly. - Arrays have no built-in `indexOf`. If you need to check membership, use a mapping as a lookup set alongside the array.
Your Local Dev Setup: Hardhat
Hardhat is to Solidity what Jest + nodemon is to Node.js. It compiles contracts, runs a local Ethereum node for testing, and provides a testing framework.
Hardhat generates TypeScript types for your contracts automatically (typechain-types/). Use them. Type-safe contract interaction is the difference between catching mistakes at compile time versus finding them by losing ETH.
The Reentrancy Vulnerability
The most important security concept to understand before you write a single line of production Solidity. A reentrancy attack happens when your contract sends ETH to an external address, and that address's fallback function calls back into your contract before the first call finishes executing.
The rule is Checks-Effects-Interactions: validate inputs, then update state, then interact with external contracts or addresses. In that order. Always.
Gas Optimization: The Things That Actually Matter
- Use `calldata` instead of `memory` for read-only function parameters.
calldataavoids copying;memorycopies. For string/array params, this can save thousands of gas. - Pack storage variables. Ethereum uses 32-byte storage slots. A
uint128+uint128fits in one slot and costs one write. Two separateuint256variables cost two writes. Order your struct fields from smallest to largest type. - Use events instead of storage for history. If you just need a record that something happened (an audit trail), emit an event instead of writing to storage. Events cost ~375 gas; storage writes cost 20,000+.
- Use `unchecked {}` for known-safe math. In tight loops where you've already validated bounds,
unchecked { i++; }skips overflow checking and saves ~40 gas per iteration. - Cache storage reads in memory variables. Every
SLOAD(storage read) costs 100 gas. If you read the same storage variable twice, cache it:uint256 bal = balances[msg.sender];then usebal.
What Surprised Me Most
After a year with Solidity, the thing that surprised me most wasn't the syntax or the tooling. It was how much the permanent, adversarial nature of the environment changes how you think about code. In web development I optimize for iteration speed. In smart contract development I optimize for correctness on the first try.
“Solidity made me a better web developer by forcing me to think rigorously about state transitions, access control, and failure modes. Those skills transfer directly back to designing APIs and database schemas.”
Before deploying to mainnet: get an audit from a firm like Trail of Bits, OpenZeppelin, or Code4rena. Not optional for anything holding real value. Your test suite is not an audit.