Minting Your First NFT Collection

Want to understand how Bored Apes, Pudgy Penguins, or Azuki actually work? Build your own NFT collection.
In the next 90-120 minutes, you'll deploy a complete NFT collection to Ethereum's test network. A real collection that appears in MetaMask, can be listed on NFT marketplaces like Rarible, and follows the same standard securing billions in NFT value. You'll learn how NFT projects generate revenue, why metadata lives off-chain, and what makes each token unique.
Note: If you've completed the ERC-20 guide, you'll recognize the Hardhat setup process. The smart contract logic is where things get interesting. NFTs work fundamentally differently from fungible tokens.
What You'll Learn
This isn't just a coding exercise. You'll understand the architecture behind every successful NFT project:
Why NFTs aren't just "JPEGs on the blockchain" - The contract stores ownership. The metadata lives on IPFS. Understanding this separation is critical to how NFTs work.
How NFT projects generate millions in revenue - Payment-gated minting is the business model. You'll implement the mechanism projects use during public sales.
Where IPFS fits in the Web3 stack - Storing images on Ethereum would cost thousands per NFT. IPFS provides decentralized storage at a fraction of the cost.
Why marketplaces like OpenSea automatically list your NFTs - Standards matter. Following ERC-721 means instant compatibility with the entire NFT ecosystem.
What You Need
MetaMask wallet
Holds test funds and signs the deployment transaction that creates your contract
Sepolia test ETH
Pays for gas fees (~$0 in real value, but required for test network)
Alchemy RPC URL
Your connection point to Ethereum. Like an API key for blockchain access.
alchemy.com (free tier)
Pinata account
Hosts your NFT images and metadata on IPFS: the decentralized storage layer
pinata.cloud (free tier)
Understanding What You're Building
Before writing code, let's understand what makes NFTs different from ERC-20 tokens.
With ERC-20 tokens, every token is identical. One USDC is exactly the same as another USDC. They're fungible, interchangeable. NFTs flip this concept. Each token is unique, identified by a specific token ID, with its own metadata describing what it represents.
Your NFT collection will work like successful projects such as Bored Ape Yacht Club or Pudgy Penguins. A fixed collection size (maxSupply), public minting for a price, and metadata pointing to IPFS storage. The contract controls who can mint and tracks which addresses own which specific tokens.
The critical difference from your stablecoin: NFTs don't have balances in the traditional sense. An address doesn't hold "5.2 NFTs". It owns token #1, token #47, and token #203. Each is distinct. Each has unique metadata. Each can have different rarity or attributes.
Now let's build this.
Project Setup With Hardhat
We'll create a new Hardhat project for your NFT collection. If you've completed the ERC-20 guide, these steps will be familiar, but we're starting fresh with a new project directory.
1. Initialize Your NFT Project
Run Hardhat's interactive initializer:
When prompted, make these selections:
Version:
Hardhat 3 Beta (recommended for new projects)Project location:
my-nft(or your preferred name)Project type:
A TypeScript Hardhat project using Node Test Runner and ViemDependencies: Confirm with
Yto install
Navigate to your new project:
2. Install OpenZeppelin Contracts
Install OpenZeppelin's audited contract library:
3. Create Your NFT Contract File
Smart Contract Development
We'll build your NFT contract step by step, inheriting from three OpenZeppelin contracts: ERC721 (core NFT functionality), ERC721URIStorage (per-token metadata), and Ownable (administrative control).
1. Initial Imports and Contract Structure
Start with the foundation. Open contracts/MyNFT.sol and add these imports that give you everything needed for a production-ready NFT contract:
What each import provides:
ERC721: The base NFT standard. Provides ownership tracking, transfers, approvals, and events. This is what makes your contract compatible with every NFT marketplace and wallet.ERC721URIStorage: Adds storage for individual token metadata URIs. Without this, all tokens would share the same base URI pattern. With it, token #1 can point to one IPFS hash while token #2 points to another. Essential for collections with unique artwork.Ownable: ProvidesonlyOwneraccess control, just like your stablecoin used. The deployer becomes the owner with special privileges: free minting for airdrops, price adjustments, and fund withdrawal.
2. State Variables and Constructor
Define what makes your collection unique: its name, symbol, supply limit, and minting price. Add this to your contracts/MyNFT.sol:
State variables:
_nextTokenId: Tracks which token ID to mint next. Starts at 0, increments with each mint.maxSupply: Hard cap on collection size. This limit is permanent. Even the owner can't exceed it.mintPrice: How much ETH users must pay to mint (stored in wei). This is where NFT projects generate revenue.
3. Public Mint Function
The core of your NFT contract: it allows anyone to mint by paying the price. Add this function to contracts/MyNFT.sol:
Key concepts:
payablemodifier: This function accepts ETH with the transaction.msg.value: The amount of ETH sent, measured in wei._safeMint(to, tokenId): OpenZeppelin's safe minting function that prevents minting to contracts that can't handle NFTs._setTokenURI(tokenId, uri): Stores a unique metadata URI for this specific token.
The payment flow: User calls mint() with ETH β Contract verifies payment β Contract mints token β Contract keeps the ETH β Owner can withdraw later.
4. Owner Mint Function
Free minting reserved for the collection owner: used for team allocation, giveaways, and promotional campaigns. Add this to contracts/MyNFT.sol:
This is identical to public mint() minus the payment requirement. The onlyOwner modifier ensures that only the contract deployer can call this function. It's useful for team allocation, giveaways, and airdrops. Still constrained by maxSupply.
5. Helper Functions
Utility functions for collection management: supply tracking, price adjustments, and revenue withdrawal. Add these to contracts/MyNFT.sol:
Function purposes:
totalSupply(): Returns how many NFTs have been minted.setMintPrice(): Allows price adjustments after deployment (owner only).withdraw(): Transfers accumulated ETH from mints to the owner.
6. Required Override Functions
These functions exist because of Solidity's multiple inheritance rules. Both ERC721 and ERC721URIStorage implement the same functions, so we must explicitly tell Solidity which implementation to use. Add these final functions to contracts/MyNFT.sol:
This is Solidity boilerplate required when inheriting from both ERC721 and ERC721URIStorage. Copy it exactly.
You're done. Your complete NFT contract in ~100 lines. It handles public minting with payment, owner privileges, metadata storage, and fund management. The same architecture securing billions in NFT value.
Writing Tests
Your NFT contract handles payments, manages ownership of unique tokens, and controls access to admin functions. Unlike your stablecoin where minting changed a balance, NFT minting involves payment verification, token ID assignment, and metadata storage. More complexity means more potential failure points
Consider what goes wrong without testing: Someone mints without paying enough. The owner accidentally sets maxSupply to 0. A user burns a token they don't own. The withdraw function fails silently. These aren't theoretical. Similar bugs have cost NFT projects millions in lost revenue or reputation damage.
We'll test using the same dual approach as your stablecoin:
Solidity tests run fast on the EVM, perfect for testing individual functions in isolation. Can users mint without paying? Does the supply cap work? Unit tests prove each function behaves correctly.
TypeScript tests simulate real user flows from start to finish. Deploy the contract, mint some NFTs, transfer one, and withdraw funds, all in sequence. These catch integration issues that unit tests miss.
Solidity Unit Tests
We'll use Foundry-style tests to verify each function works correctly, including all edge cases and failure modes.
1. Setup
Create the test file:
Add the test contract with setup function:
Setup notes:
vm.deal(user1, 10 ether): Foundry cheatcode that gives addresses ETH for minting.SAMPLE_URI: Placeholder IPFS hash for testing.
2. Test Deployment State
Verify the contract initializes correctly:
NFT collections start empty. Users mint tokens after deployment.
3. Test Public Minting
Test the payment-based minting flow:
Key patterns:
{value: MINT_PRICE}: Sends ETH with the function call.address(nft).balance: Checks the contract's accumulated ETH.ownerOf(tokenId): Returns the owner of a specific token.tokenURI(tokenId): Returns the metadata URI for a token.
4. Test Owner Minting
Verify owner can mint for free and non-owners cannot:
5. Test Price Management
Verify only the owner can adjust pricing:
6. Test Withdrawals
Verify the owner can collect sales revenue:
7. Test ERC721 Standard
Verify token transfers work correctly:
These verify ERC-721 compliance. Marketplaces rely on these functions working correctly for trading NFTs.
8. Run Solidity Tests
Execute your test suite:
You should see all tests passing, confirming your contract logic is solid.
TypeScript End-to-End Tests
Now we'll test a realistic user journey: deploy, mint multiple NFTs, adjust price, and withdraw funds.
1. Setup
Create the TypeScript test file:
Add the test structure:
2. Implement Complete NFT Lifecycle Test
This single test covers the entire NFT project lifecycle:
What this test demonstrates:
This comprehensive test simulates a real NFT project launch:
Deployment: Create the collection with configuration
Team allocation: Owner mints tokens for free (airdrops/marketing)
Public sale: Users buy NFTs by paying the mint price
Payment validation: Insufficient payment gets rejected
Dynamic pricing: Owner adjusts price for different sale phases
Trading: NFTs can be transferred between users
Revenue collection: Owner withdraws accumulated ETH
This is the actual flow of a successful NFT launch. Your test proves every step works.
3. Run TypeScript Tests
Execute the integration test:
You should see detailed console output walking through each phase of the NFT lifecycle, confirming everything works together.
Your deployed smart contract controls ownership and minting logic. But NFTs need something more: metadata describing what each token actually represents. NFTs need metadata: the JSON files describing each token's name, description, image, and attributes. This metadata lives off-chain (storing it on Ethereum would cost thousands per NFT), typically on IPFS for decentralized, permanent storage.
NFT metadata follows the ERC-721 Metadata Standard and OpenSea's metadata conventions. Here's what a typical metadata file looks like:
Field breakdown:
name: The token's display name (shows in wallets and marketplaces)description: A brief explanation of what this NFT representsimage: IPFS URI pointing to the actual artwork fileattributes: Traits that define rarity and characteristics (used for filtering on OpenSea)
Setting Up Pinata for IPFS Storage
Pinata is a pinning service that ensures your files stay available on IPFS. Without pinning, IPFS files can disappear when no nodes are hosting them.
1. Create Your Pinata Account
Visit pinata.cloud and sign up for a free account
You'll upload files directly through the Pinata dashboard
2. Prepare Sample NFT Assets
For this tutorial, we'll create a small 5-NFT collection. You'll need:
Images: Create or download 5 images (PNG or JPG format, ideally 1000x1000px or larger). Name them 1.png, 2.png, 3.png, 4.png, 5.png.
For testing purposes, you can use any images: abstract patterns, AI-generated art, or simple colored squares. Production NFT projects invest heavily in artwork, but for learning, placeholder images work fine.
Create a folder structure in your project:
Place your images in nft-assets/images/.
3. Upload Images to IPFS (First CID)
β οΈ Important: You'll upload TWO folders separately and get TWO different CIDs:
Images CID - for your artwork files (this step)
Metadata CID - for your JSON files (step 5)
Upload your images first:
Log into your Pinata dashboard
Click Upload β Folder
Select your
nft-assets/imagesfolderPinata will upload all images and return a CID (Content Identifier)
Save this Images CID (it looks like:
QmX7J9r8UzL2K3M4N5P6Q8R9S0T1U2V3W4X5Y6Z7A8B9C0)
Your images are now on IPFS. Each image is accessible at:
Now create 5 JSON files in nft-assets/metadata/, one for each NFT.
β οΈ Use your Images CID from step 3 in the "image" field. This is where the two CIDs connect!
1.json:
2.json:
Repeat for 3.json, 4.json, and 5.json, adjusting the token numbers and attributes.
Pro tip: Real NFT projects generate these files programmatically. With 10,000 NFTs, you'd write a script combining trait layers. For 5 NFTs, manual creation is faster.
Now upload the metadata folder to get a separate CID:
Return to Pinata dashboard
Click Upload β Folder
Select your
nft-assets/metadatafolderSave this Metadata CID (this will be different from your Images CID!)
Your metadata is now on IPFS! Each metadata file is accessible at:
These Metadata URIs are what you'll pass to your contract's mint() function
Summary: Two CIDs, Two Purposes
Images CID
Your artwork files (.png,.jpg)
Inside metadata JSON files ("image" field)
ipfs://QmImages.../1.png
Metadata CID
Your JSON files
In your mint() function call
ipfs://QmMeta.../1.json
The flow works like this: mint() calls tokenURI, which points to Metadata JSON. The "image" field in that JSON points to the actual artwork.
Why IPFS and Not Regular Cloud Storage?
You might wonder: Why not just host these files on AWS S3 or a web server?
Decentralization: IPFS ensures your NFTs remain accessible even if Pinata shuts down. Any IPFS node can serve the content. AWS could delete your files, take down your bucket, or ban your account.
Immutability: IPFS addresses (CIDs) are derived from file content. If someone changes the file, the CID changes. This cryptographic link between address and content guarantees your NFT's metadata can't be secretly modified.
Standard practice: The NFT ecosystem expects ipfs:// URIs. Marketplaces render them automatically. Using HTTP URLs marks your project as amateur and raises concerns about rug pulls (projects that later replace valuable artwork with blank images).
That said, some projects use hybrid approaches: images on IPFS, metadata on fast CDNs. But for learning and most projects, full IPFS is the gold standard.
Setting Up for Deployment
With your contract tested and metadata prepared, it's time to deploy your NFT collection to a public network. We'll use the Sepolia testnet. The same process as mainnet deployment, but with free test ETH.
1. Setting Up Environment Variables
Just like in the ERC-20 guide, we'll use Hardhat's secure keystore to manage sensitive credentials.
Set SEPOLIA_RPC_URL: Run the command below, create a keystore password, and paste your Alchemy RPC URL.
Set SEPOLIA_PRIVATE_KEY: Store your wallet's private key securely. This key signs the deployment transaction.
β οΈ Security warning: Your private key controls your funds. Never commit it to version control or share it with anyone.
2. Create the Deployment Script
We'll use Hardhat Ignition for reliable, repeatable deployments.
Create the deployment file:
Setup the deployment script:
Parameter decisions:
maxSupply: Set to 100 for this tutorial.mintPrice: 0.01 ETH is reasonable for testnets.Adjust these values based on your collection's goals.
3. Deploy Your NFT Collection
Execute the deployment:
You'll see output like this:
Save your contract address. You'll need it for minting and verification.
Interacting with Your NFT Contract
Your collection is live on Sepolia. Let's mint some NFTs using the IPFS metadata you prepared earlier.
Creating a Mint Script
We'll create a script to mint your first NFT. This script connects to your deployed contract and calls the mint() function with payment.
Create the script:
Add the minting logic:
Run the mint script:
Mint multiple NFTs:
To mint your entire collection, modify the script to loop through your metadata files:
Viewing Your NFTs on Rarible
Rarible's testnet marketplace automatically indexes Sepolia NFTs. To see your collection:
Visit testnet.rarible.com
Connect your MetaMask wallet (ensure you're on Sepolia network)
Navigate to your profile
Your minted NFTs should appear within a few minutes
Alternatively, view your collection directly by constructing the URL:
Replace YOUR_CONTRACT_ADDRESS with your deployed contract address and TOKEN_ID with the token number (0, 1, 2, etc.).
Troubleshooting: If NFTs don't appear immediately:
Wait a few minutes for Rarible's indexer to process the contract
Verify your transaction succeeded on Sepolia Etherscan
Check that your IPFS metadata is accessible via Pinata's gateway
Using Your NFT Contract's Admin Functions
As the contract owner, you have special privileges. Let's test them.
Free owner mint (airdrop):
Create scripts/ownerMint.ts:
Update mint price:
Withdraw collected funds:
After people mint, you'll want to collect the accumulated ETH:
Conclusion
ou've built and deployed a complete NFT collection. Here's what you accomplished:
Built a production-ready ERC-721 smart contract with public minting, owner privileges, supply caps, and revenue collection. The same architecture powering billion-dollar NFT projects.
Mastered NFT metadata standards by creating properly structured JSON files, uploading them to IPFS via Pinata, and understanding why decentralized storage matters.
Implemented comprehensive testing using both Solidity unit tests and TypeScript integration tests, covering payment validation, access control, and the complete NFT lifecycle.
Deployed to a public blockchain using Hardhat Ignition and secure credential management, making your collection accessible to anyone with an Ethereum wallet.
Minted real NFTs that appear in MetaMask and on Rarible, experiencing the same flow users follow when participating in NFT drops.
What You Built vs. Production NFT Projects
Your contract contains the core mechanisms behind successful NFT collections:
Payment-gated minting: The revenue model that generated $2 billion+ during the 2021-2022 NFT boom
Supply caps: The scarcity mechanism that drives value (Bored Apes' 10,000 limit)
Metadata linking: The IPFS integration that separates ownership (on-chain) from assets (off-chain)
Owner controls: The administrative functions teams use for airdrops and price management
What's missing from production projects?
Whitelist/allowlist: Many projects restrict early mints to approved addresses
Reveal mechanics: Some collections hide metadata initially, revealing artwork after sellout
Royalties: ERC-2981 standard for automatic creator fees on secondary sales
Gas optimization: Techniques like ERC-721A for batch minting at reduced cost
These are enhancements, not fundamentals. You've built the foundation.
Last updated