Learn how to structure NFT metadata and store it on decentralized storage systems like IPFS.
{
"name": "Cool Cat #1234",
"description": "One of 10,000 unique Cool Cats",
"image": "ipfs://QmXyz.../1234.png",
"external_url": "https://coolcats.com/1234",
"attributes": [
{
"trait_type": "Background",
"value": "Blue"
},
{
"trait_type": "Body",
"value": "Orange"
},
{
"trait_type": "Hat",
"value": "Cowboy"
},
{
"display_type": "number",
"trait_type": "Generation",
"value": 1
}
]
}{
"attributes": [
{
"trait_type": "Level",
"value": 5
},
{
"display_type": "boost_number",
"trait_type": "Stamina Increase",
"value": 10
},
{
"display_type": "boost_percentage",
"trait_type": "Speed Boost",
"value": 25
},
{
"display_type": "date",
"trait_type": "Birthday",
"value": 1640995200
}
]
}IPFS (InterPlanetary File System) is a peer-to-peer distributed file system for storing and sharing files.
- ✅ Decentralized: No single point of failure
- ✅ Permanent: Content-addressed storage
- ✅ Immutable: Files can't be changed
- ✅ Efficient: Deduplication
ipfs://QmXyz.../metadata.json
Resolves to:
https://ipfs.io/ipfs/QmXyz.../metadata.json
https://gateway.pinata.cloud/ipfs/QmXyz.../metadata.json
const pinataSDK = require('@pinata/sdk');
const fs = require('fs');
const pinata = new pinataSDK(PINATA_API_KEY, PINATA_SECRET);
// Upload image
async function uploadImage(filePath) {
const readableStream = fs.createReadStream(filePath);
const options = {
pinataMetadata: {
name: 'NFT Image'
}
};
const result = await pinata.pinFileToIPFS(readableStream, options);
console.log('Image CID:', result.IpfsHash);
return result.IpfsHash;
}
// Upload metadata
async function uploadMetadata(metadata) {
const options = {
pinataMetadata: {
name: 'NFT Metadata'
}
};
const result = await pinata.pinJSONToIPFS(metadata, options);
console.log('Metadata CID:', result.IpfsHash);
return result.IpfsHash;
}
// Complete upload flow
async function uploadNFT() {
// 1. Upload image
const imageCID = await uploadImage('./images/nft.png');
// 2. Create metadata
const metadata = {
name: "My NFT",
description: "An awesome NFT",
image: `ipfs://${imageCID}`,
attributes: [
{ trait_type: "Rarity", value: "Rare" }
]
};
// 3. Upload metadata
const metadataCID = await uploadMetadata(metadata);
return `ipfs://${metadataCID}`;
}import { NFTStorage, File } from 'nft.storage';
const client = new NFTStorage({ token: NFT_STORAGE_KEY });
async function storeNFT(imagePath, name, description) {
const imageFile = await fs.promises.readFile(imagePath);
const metadata = await client.store({
name: name,
description: description,
image: new File([imageFile], 'nft.png', { type: 'image/png' }),
attributes: [
{ trait_type: "Type", value: "Cool" }
]
});
console.log('Metadata URL:', metadata.url);
return metadata.url;
}const fs = require('fs');
function generateMetadata(tokenId, traits) {
return {
name: `Cool Cat #${tokenId}`,
description: "Part of the Cool Cats collection",
image: `ipfs://QmBase/${tokenId}.png`,
attributes: Object.entries(traits).map(([trait_type, value]) => ({
trait_type,
value
}))
};
}
async function generateCollection(count) {
for (let i = 1; i <= count; i++) {
const traits = {
"Background": randomChoice(backgrounds),
"Body": randomChoice(bodies),
"Eyes": randomChoice(eyes),
"Hat": randomChoice(hats)
};
const metadata = generateMetadata(i, traits);
// Save to file
fs.writeFileSync(
`./metadata/${i}.json`,
JSON.stringify(metadata, null, 2)
);
}
}
function randomChoice(array) {
return array[Math.floor(Math.random() * array.length)];
}class RarityGenerator {
constructor() {
this.traits = {
background: [
{ value: "Blue", weight: 50 },
{ value: "Red", weight: 30 },
{ value: "Gold", weight: 20 }
],
hat: [
{ value: "None", weight: 60 },
{ value: "Cap", weight: 30 },
{ value: "Crown", weight: 10 }
]
};
}
selectTrait(traitName) {
const options = this.traits[traitName];
const totalWeight = options.reduce((sum, opt) => sum + opt.weight, 0);
let random = Math.random() * totalWeight;
for (const option of options) {
if (random < option.weight) {
return option.value;
}
random -= option.weight;
}
}
generate() {
return {
background: this.selectTrait('background'),
hat: this.selectTrait('hat')
};
}
}Pros:
- ✅ Cheaper (no storage costs)
- ✅ Can include large files
- ✅ Easy to implement
Cons:
- ❌ Requires IPFS pinning
- ❌ Gateway dependency
function tokenURI(uint256 tokenId) public view returns (string memory) {
return string(abi.encodePacked(baseURI, tokenId.toString(), ".json"));
}Pros:
- ✅ Fully decentralized
- ✅ No external dependencies
- ✅ Permanent
Cons:
- ❌ Expensive (gas costs)
- ❌ Size limitations
contract OnChainNFT is ERC721 {
mapping(uint256 => string) private _tokenURIs;
function tokenURI(uint256 tokenId) public view override returns (string memory) {
string memory json = Base64.encode(
bytes(
string(
abi.encodePacked(
'{"name": "NFT #',
tokenId.toString(),
'", "description": "On-chain NFT", "image": "data:image/svg+xml;base64,',
generateSVG(tokenId),
'"}'
)
)
)
);
return string(abi.encodePacked('data:application/json;base64,', json));
}
function generateSVG(uint256 tokenId) private pure returns (string memory) {
// Generate SVG on-chain
return Base64.encode(bytes('<svg>...</svg>'));
}
}contract DynamicNFT is ERC721 {
mapping(uint256 => uint256) public birthTime;
function tokenURI(uint256 tokenId) public view override returns (string memory) {
uint256 age = block.timestamp - birthTime[tokenId];
string memory stage = getLifeStage(age);
return string(abi.encodePacked(baseURI, stage, "/", tokenId.toString(), ".json"));
}
function getLifeStage(uint256 age) private pure returns (string memory) {
if (age < 7 days) return "baby";
if (age < 30 days) return "child";
return "adult";
}
}// Update metadata based on usage
async function evolveNFT(tokenId, newLevel) {
const currentMetadata = await fetchMetadata(tokenId);
const updatedMetadata = {
...currentMetadata,
attributes: currentMetadata.attributes.map(attr =>
attr.trait_type === "Level"
? { ...attr, value: newLevel }
: attr
)
};
// Upload new metadata
const newCID = await uploadMetadata(updatedMetadata);
// Update on-chain pointer
await contract.setTokenURI(tokenId, `ipfs://${newCID}`);
}- ✅ Use IPFS URIs in smart contracts, not gateway URLs
- ✅ Pin content on multiple services
- ✅ Validate metadata before upload
- ✅ Use CIDv1 for better compatibility
- ✅ Include fallback gateways
- ✅ Optimize images for web viewing
- ✅ Document attributes clearly
- ✅ Test metadata rendering
- Pinata - IPFS pinning service
- NFT.Storage - Free IPFS for NFTs
- Infura IPFS - IPFS API
- Web3.Storage - Decentralized storage
Next: NFT Marketplaces →