NFTs — Creation, Trading and Storage Explained
This tutorial covers NFT fundamentals — you will learn how non-fungible tokens work on Ethereum, how to create and mint a collection, how marketplaces facilitate trading, and how to store NFTs securely.
Why It Matters
NFTs represent a paradigm shift in digital ownership. For the first time, digital assets can be provably scarce, verifiably authentic, and truly owned by their purchasers — not locked inside a platform's database.
Real-World Use
An artist mints a limited edition of 100 digital artworks and earns royalties on every secondary sale. A musician releases an album where each copy is an NFT granting access to exclusive content. A game developer sells in-game items as NFTs that players can trade freely outside the game.
flowchart LR
A[Creator] --> B[Mint NFT]
B --> C[Collection Smart Contract]
C --> D[Marketplace Listing]
D --> E[Buyer Purchases]
E --> F[NFT Transferred to Buyer]
F --> G[Creator Earns Royalty on Resale]
style A fill:#4CAF50,color:#fff
style C fill:#EF7B4D,color:#fff
style F fill:#2196F3,color:#fff
style G fill:#FF9800,color:#fff
ERC-721 Standard
The ERC-721 standard defines the interface for non-fungible tokens. Each token has a unique tokenId and a single owner.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC721 {
function ownerOf(uint256 tokenId) external view returns (address);
function transferFrom(address from, address to, uint256 tokenId) external;
function safeTransferFrom(address from, address to, uint256 tokenId) external;
}
contract SimpleNFT is IERC721 {
mapping(uint256 => address) private _owners;
mapping(address => uint256) private _balances;
uint256 private _nextTokenId;
function mint(address to) public returns (uint256) {
uint256 tokenId = _nextTokenId++;
_owners[tokenId] = to;
_balances[to]++;
return tokenId;
}
function ownerOf(uint256 tokenId) external view override returns (address) {
return _owners[tokenId];
}
function transferFrom(address from, address to, uint256 tokenId) external override {
require(_owners[tokenId] == from, "Not the owner");
require(msg.sender == from, "Caller is not the owner");
_owners[tokenId] = to;
_balances[from]--;
_balances[to]++;
}
function safeTransferFrom(address from, address to, uint256 tokenId) external override {
transferFrom(from, to, tokenId);
}
}
Expected behavior: call mint(0x123...) — returns token ID 0. Call ownerOf(0) — returns 0x123.... Call transferFrom(0x123..., 0x456..., 0) — ownership transfers. Calling transferFrom from a non-owner reverts with "Not the owner".
Minting an NFT Collection
Most NFT projects use a Factory contract that mints tokens with metadata. The metadata is typically stored as an IPFS URI pointing to a JSON file.
// Example mint script using ethers.js
const { ethers } = require("ethers");
const abi = [
"function mint(address to, string memory uri) returns (uint256)",
"function tokenURI(uint256 tokenId) view returns (string)]
];
const provider = new ethers.JsonRpcProvider("https://rpc.sepolia.org");
const wallet = new ethers.Wallet("YOUR_PRIVATE_KEY", provider);
const contract = new ethers.Contract("0xCONTRACT_ADDRESS", abi, wallet);
async function mintNFT() {
const metadataURI = "ipfs://QmXyZ.../metadata.json";
const tx = await contract.mint(wallet.address, metadataURI);
const receipt = await tx.wait();
console.log(`Minted in block: ${receipt.blockNumber}`);
console.log(`Transaction hash: ${receipt.hash}`);
const uri = await contract.tokenURI(0);
console.log(`Token URI: ${uri}`);
}
mintNFT();
Expected output:
Minted in block: 4567890
Transaction hash: 0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890
Token URI: ipfs://QmXyZ.../metadata.json
NFT Trading on Marketplaces
Marketplaces like OpenSea and Blur facilitate peer-to-peer NFT trading. Sellers create listing orders off-chain, buyers fill them on-chain.
contract SimpleMarketplace {
struct Listing {
address seller;
uint256 tokenId;
uint256 price;
bool active;
}
IERC721 public nftContract;
mapping(uint256 => Listing) public listings;
constructor(address _nftContract) {
nftContract = IERC721(_nftContract);
}
function list(uint256 tokenId, uint256 price) public {
require(nftContract.ownerOf(tokenId) == msg.sender, "Not owner");
nftContract.transferFrom(msg.sender, address(this), tokenId);
listings[tokenId] = Listing(msg.sender, tokenId, price, true);
}
function buy(uint256 tokenId) public payable {
Listing memory listing = listings[tokenId];
require(listing.active, "Not listed");
require(msg.value == listing.price, "Incorrect price");
listings[tokenId].active = false;
nftContract.transferFrom(address(this), msg.sender, tokenId);
payable(listing.seller).transfer(msg.value);
}
}
Expected behavior: seller calls list(5, 1 ether) — token 5 is transferred to the marketplace contract. Buyer calls buy(5) with 1 ETH — token 5 transfers to buyer, seller receives 1 ETH. Attempting to buy with 0.9 ETH reverts with "Incorrect price".
Storage Best Practices
NFT metadata and images should never be stored on centralized servers. IPFS (InterPlanetary File System) provides content-addressed storage that ensures availability and immutability.
import ipfshttpclient
client = ipfshttpclient.connect('/ip4/127.0.0.1/tcp/5001')
metadata = {
"name": "Artwork #1",
"description": "A unique digital artwork",
"image": "ipfs://QmImageHash...",
"attributes": [
{"trait_type": "Background", "value": "Sunset"},
{"trait_type": "Rarity", "value": "Legendary"}
]
}
import json
metadata_bytes = json.dumps(metadata).encode('utf-8')
res = client.add_bytes(metadata_bytes)
print(f"Metadata CID: {res}")
Expected output:
Metadata CID: QmXyZabc123def456...
The returned CID is the content identifier. Setting tokenURI(0) to ipfs://QmXyZabc123def456... ensures the metadata is permanently accessible through the IPFS network.
Common Errors
- Storing metadata on centralized servers — If the server goes down, the NFT displays as a broken link. Always pin to IPFS or Arweave.
- Not testing on testnet first — A failed mint on mainnet wastes gas and may not be recoverable. Always deploy and verify on Sepolia first.
- Ignoring royalty enforcement — ERC-721 does not enforce royalties on-chain. Use ERC-2981 or a marketplace-enforced registry.
- Reuse of token IDs — Deleting a token and minting with the same ID can confuse wallets and marketplaces. Use sequential IDs or burn tracking.
- Minting without URI validation — A malformed metadata URI can make the NFT unreadable. Validate URIs before accepting them in mint functions.
Practice Questions
What is the difference between ERC-721 and ERC-1155? ERC-721 creates a unique contract per token type, each with its own ownership mapping. ERC-1155 manages multiple token types in a single contract, supporting both fungible and non-fungible tokens with batch operations.
How do NFT royalties work on secondary sales? EIP-2981 defines a standard royalty interface. Marketplaces that support it pay a percentage of the sale price to the original creator. Off-chain enforcement means not all marketplaces honor on-chain royalty declarations.
Why is IPFS preferred over HTTP for NFT metadata? IPFS uses content-addressing — the URI is a hash of the content. If the content changes, the hash changes. This guarantees immutability and verifiability. HTTP URLs can be changed or deleted by the server operator.
Frequently Asked Questions
{{< faq question="Do I own the copyright when I buy an NFT?">}} Owning an NFT gives you ownership of the token on-chain, but copyright is a separate legal right. Unless the license explicitly transfers copyright (e.g., Creative Commons or a custom NFT license), you only own the token — not the underlying artwork. Always read the terms before purchasing. {{< /faq >}}
{{< faq question="What happens if the NFT platform goes offline?">}} If the marketplace shuts down, your NFTs remain in your wallet because they exist on the Blockchain — not on the platform's servers. You can still transfer, sell, or view them through any other platform that reads the same Blockchain. Metadata and images stored on IPFS remain accessible as long as at least one node pins them. {{< /faq >}}
{{< faq question="How much does it cost to mint an NFT on Ethereum?">}} Costs vary with gas price and contract complexity. On Ethereum mainnet, a simple mint costs $10-$100 at average gas prices. On Layer 2 networks like Polygon or Arbitrum, minting costs less than $0.01. Most new projects launch on L2s for this reason.{{< /faq >}}
Next Steps
Smart Contract Development with Solidity — Build the skills to create your own NFT contract.
Building Web3 Frontends with ethers.js — Connect your NFT contract to a web interface.
Layer 2 Scaling Solutions — Mint NFTs for a fraction of mainnet cost.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro