· Nacho Coll · Guides · 9 min read
NFT Metadata Storage: The Complete Guide to IPFS for NFT Creators
Step-by-step guide to storing NFT metadata on IPFS. Includes ERC-721 tokenURI patterns, Python and JavaScript examples.

Creating NFTs requires more than just smart contract deployment – you need reliable, decentralized storage for your metadata and assets. This comprehensive guide walks you through storing NFT metadata on IPFS using industry best practices, complete with code examples for Python and JavaScript developers.

Why IPFS for NFT Metadata Storage?
Traditional web hosting creates centralization risks for NFT projects. When metadata lives on conventional servers, NFTs can become “broken” if the hosting service goes down or changes URLs. IPFS (InterPlanetary File System) solves this by providing:
- Immutable content addressing: Each file gets a unique Content Identifier (CID) that never changes
- Decentralized storage: Content exists across multiple nodes worldwide
- Cryptographic verification: File integrity is guaranteed through content hashing
- Future-proof URLs: IPFS links work indefinitely, protecting long-term value
For a deeper understanding of IPFS fundamentals, check out our guide on what is IPFS pinning.
Understanding ERC-721 Metadata Structure
The ERC-721 standard defines how NFT metadata should be structured. Your smart contract’s tokenURI function returns a URL pointing to JSON metadata following this pattern:
{
"name": "My Amazing NFT #1",
"description": "A unique digital artwork showcasing...",
"image": "ipfs://QmYourImageCIDHere",
"attributes": [
{
"trait_type": "Background",
"value": "Blue"
},
{
"trait_type": "Rarity",
"value": "Common"
}
],
"external_url": "https://yourproject.com/token/1"
}Key Metadata Fields
- name: The NFT’s title displayed in wallets and marketplaces
- description: Detailed information about the NFT
- image: IPFS URL to the main visual asset
- attributes: Trait-based properties for filtering and rarity calculations
- external_url: Optional link to additional content or your project website
Step-by-Step IPFS NFT Storage Process
Step 1: Prepare Your Assets and Metadata
Before uploading anything, organize your files:
- Main assets: Images, videos, or other primary content
- Metadata files: JSON files describing each NFT
- Collection metadata: Optional collection-level information
Step 2: Upload Assets to IPFS
Start by uploading your main NFT assets (images, videos, etc.) to get their IPFS CIDs. You’ll reference these CIDs in your metadata JSON files.
Here’s how to upload an image using Python:
import requests
import base64
import json
def upload_image_to_ipfs(image_path, api_key):
"""Upload an image file to IPFS and return its CID"""
# Read and encode image
with open(image_path, 'rb') as f:
image_data = base64.b64encode(f.read()).decode('utf-8')
# Prepare upload payload
payload = {
"content": image_data,
"description": f"NFT Asset: {image_path}"
}
headers = {
"Content-Type": "application/json",
"X-Api-Key": api_key
}
# Upload to IPFS.NINJA
response = requests.post(
"https://api.ipfs.ninja/upload/new",
headers=headers,
json=payload
)
if response.status_code == 200:
result = response.json()
print(f"✅ Image uploaded successfully!")
print(f"CID: {result['cid']}")
print(f"IPFS URL: {result['uris']['ipfs']}")
print(f"Gateway URL: {result['uris']['url']}")
return result['cid']
else:
print(f"❌ Upload failed: {response.text}")
return None
# Example usage
API_KEY = "bws_1234567890abcdef1234567890abcdef" # Replace with your actual key
image_cid = upload_image_to_ipfs("my-nft-artwork.png", API_KEY)Step 3: Create and Upload Metadata JSON
Once you have your asset CIDs, create metadata JSON files and upload them:
def create_and_upload_metadata(name, description, image_cid, attributes, api_key):
"""Create NFT metadata JSON and upload to IPFS"""
# Create metadata object
metadata = {
"name": name,
"description": description,
"image": f"ipfs://{image_cid}",
"attributes": attributes
}
# Convert to JSON string and encode
metadata_json = json.dumps(metadata, indent=2)
metadata_b64 = base64.b64encode(metadata_json.encode('utf-8')).decode('utf-8')
# Upload metadata
payload = {
"content": metadata_b64,
"description": f"NFT Metadata: {name}",
"metadata": {
"contentType": "application/json",
"nftTokenId": name.split('#')[1] if '#' in name else "1"
}
}
headers = {
"Content-Type": "application/json",
"X-Api-Key": api_key
}
response = requests.post(
"https://api.ipfs.ninja/upload/new",
headers=headers,
json=payload
)
if response.status_code == 200:
result = response.json()
print(f"✅ Metadata uploaded successfully!")
print(f"Metadata CID: {result['cid']}")
return result['cid']
else:
print(f"❌ Metadata upload failed: {response.text}")
return None
# Example usage
attributes = [
{"trait_type": "Background", "value": "Cosmic Blue"},
{"trait_type": "Eyes", "value": "Laser"},
{"trait_type": "Rarity", "value": "Epic"}
]
metadata_cid = create_and_upload_metadata(
name="Cosmic Warrior #001",
description="A fierce warrior from the distant galaxies, wielding the power of stars.",
image_cid=image_cid,
attributes=attributes,
api_key=API_KEY
)Step 4: JavaScript Implementation
For web applications or Node.js projects, here’s the JavaScript equivalent:
class NFTStorage {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = 'https://api.ipfs.ninja';
}
async uploadFile(fileContent, description) {
const payload = {
content: fileContent, // base64 encoded
description: description
};
const response = await fetch(`${this.baseUrl}/upload/new`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': this.apiKey
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.statusText}`);
}
return await response.json();
}
async uploadImageFromFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = async (e) => {
try {
const base64Content = e.target.result.split(',')[1]; // Remove data:image/...;base64, prefix
const result = await this.uploadFile(base64Content, `NFT Image: ${file.name}`);
resolve(result.cid);
} catch (error) {
reject(error);
}
};
reader.readAsDataURL(file);
});
}
async uploadMetadata(name, description, imageCid, attributes = []) {
const metadata = {
name,
description,
image: `ipfs://${imageCid}`,
attributes
};
const metadataJson = JSON.stringify(metadata, null, 2);
const base64Metadata = btoa(metadataJson);
const result = await this.uploadFile(base64Metadata, `NFT Metadata: ${name}`);
return result.cid;
}
}
// Usage example
const storage = new NFTStorage('bws_1234567890abcdef1234567890abcdef'); // Replace with your key
// Upload process
async function createNFT() {
try {
// Assuming you have a file input element
const fileInput = document.getElementById('nft-image');
const imageFile = fileInput.files[0];
console.log('Uploading image...');
const imageCid = await storage.uploadImageFromFile(imageFile);
console.log(`Image uploaded: ${imageCid}`);
console.log('Uploading metadata...');
const metadataCid = await storage.uploadMetadata(
'Galaxy Explorer #042',
'A mysterious explorer traversing the cosmic void.',
imageCid,
[
{ trait_type: 'Class', value: 'Explorer' },
{ trait_type: 'Galaxy', value: 'Andromeda' },
{ trait_type: 'Rarity', value: 'Legendary' }
]
);
console.log(`Metadata uploaded: ${metadataCid}`);
console.log(`Token URI: ipfs://${metadataCid}`);
} catch (error) {
console.error('Upload failed:', error);
}
}Implementing tokenURI in Your Smart Contract
Once your metadata is uploaded to IPFS, implement the tokenURI function in your ERC-721 contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyNFTCollection is ERC721, Ownable {
mapping(uint256 => string) private _tokenURIs;
string private _baseTokenURI;
constructor(string memory name, string memory symbol) ERC721(name, symbol) {}
function setTokenURI(uint256 tokenId, string memory uri) external onlyOwner {
require(_exists(tokenId), "Token does not exist");
_tokenURIs[tokenId] = uri;
}
function setBaseURI(string memory baseURI) external onlyOwner {
_baseTokenURI = baseURI;
}
function tokenURI(uint256 tokenId) public view virtual override returns (string) {
require(_exists(tokenId), "Token does not exist");
string memory _tokenURI = _tokenURIs[tokenId];
// Return specific token URI if set
if (bytes(_tokenURI).length > 0) {
return _tokenURI;
}
// Fall back to base URI + token ID pattern
if (bytes(_baseTokenURI).length > 0) {
return string(abi.encodePacked(_baseTokenURI, tokenId.toString()));
}
return "";
}
function mintWithURI(address to, uint256 tokenId, string memory uri) external onlyOwner {
_mint(to, tokenId);
_tokenURIs[tokenId] = uri;
}
}Batch Operations for Large Collections
For large NFT collections, batch operations save time and gas costs:
def batch_upload_collection(collection_data, api_key):
"""Upload an entire NFT collection in batches"""
print(f"Starting batch upload of {len(collection_data)} NFTs...")
results = []
for i, nft_data in enumerate(collection_data):
print(f"Processing NFT {i+1}/{len(collection_data)}: {nft_data['name']}")
try:
# Upload image
image_cid = upload_image_to_ipfs(nft_data['image_path'], api_key)
if image_cid:
# Upload metadata
metadata_cid = create_and_upload_metadata(
name=nft_data['name'],
description=nft_data['description'],
image_cid=image_cid,
attributes=nft_data['attributes'],
api_key=api_key
)
if metadata_cid:
results.append({
'token_id': i + 1,
'name': nft_data['name'],
'image_cid': image_cid,
'metadata_cid': metadata_cid,
'token_uri': f"ipfs://{metadata_cid}"
})
except Exception as e:
print(f"❌ Error processing {nft_data['name']}: {e}")
print(f"✅ Batch upload complete! {len(results)} NFTs processed successfully.")
return results
# Example collection data
collection_data = [
{
'name': 'Cosmic Warrior #001',
'description': 'A fierce warrior from distant galaxies.',
'image_path': 'images/warrior_001.png',
'attributes': [
{'trait_type': 'Class', 'value': 'Warrior'},
{'trait_type': 'Rarity', 'value': 'Epic'}
]
},
# Add more NFTs...
]
results = batch_upload_collection(collection_data, API_KEY)Best Practices for NFT Metadata Storage
1. Use Descriptive File Names
When uploading to IPFS, use meaningful descriptions to help with organization:
payload = {
"content": base64_content,
"description": f"Collection: {collection_name} | Token: {token_id} | Type: {file_type}"
}2. Implement Proper Error Handling
Always handle upload failures gracefully:
import time
def upload_with_retry(upload_function, max_retries=3, delay=2):
"""Upload with exponential backoff retry logic"""
for attempt in range(max_retries):
try:
return upload_function()
except Exception as e:
if attempt == max_retries - 1:
raise e
print(f"Attempt {attempt + 1} failed: {e}. Retrying in {delay} seconds...")
time.sleep(delay)
delay *= 2 # Exponential backoff3. Validate Metadata Structure
Ensure your metadata follows standards:
def validate_metadata(metadata):
"""Validate NFT metadata structure"""
required_fields = ['name', 'description', 'image']
for field in required_fields:
if field not in metadata:
raise ValueError(f"Missing required field: {field}")
if not metadata['image'].startswith('ipfs://'):
raise ValueError("Image must be an IPFS URL")
if 'attributes' in metadata:
for attr in metadata['attributes']:
if 'trait_type' not in attr or 'value' not in attr:
raise ValueError("Invalid attribute structure")
return TrueChoosing the Right IPFS Pinning Service
When selecting an IPFS pinning service for your NFT project, consider:
- Reliability: Guaranteed uptime for long-term storage
- Performance: Fast retrieval speeds worldwide
- Pricing: Cost-effective for your collection size
- Features: API capabilities, analytics, and developer tools
For a detailed comparison of pinning services, read our IPFS Ninja vs Pinata comparison and our guide to the best IPFS pinning services.
Advanced Features: Custom Gateways and Analytics
IPFS Ninja offers additional features for professional NFT projects:
Custom Gateway Configuration
Create branded IPFS gateways for your collection:
// Access your NFT through a custom gateway
const customGateway = 'https://my-collection.gw.ipfs.ninja';
const nftUrl = `${customGateway}/ipfs/${metadataCid}`;Upload Analytics
Monitor your NFT storage usage and access patterns through the dashboard analytics, helping you understand collection performance and optimize storage costs.
Troubleshooting Common Issues
Metadata Not Loading
- Verify IPFS URLs use
ipfs://protocol - Check that metadata JSON is valid
- Ensure pinning service is maintaining the content
Images Not Displaying
- Confirm image CIDs are correct in metadata
- Test image URLs in IPFS gateways
- Verify image file formats are web-compatible
Gas Estimation Errors
- Ensure
tokenURIfunction returns valid strings - Check for circular references in metadata
- Validate all IPFS CIDs before minting
Monitoring and Maintaining Your NFT Storage
After deploying your collection:
- Regular Health Checks: Verify metadata and images remain accessible
- Backup Important CIDs: Keep records of all uploaded content identifiers
- Monitor Analytics: Track access patterns and storage usage
- Plan for Scale: Consider upgrading your pinning service as your collection grows
For more details on managing uploads programmatically, see our IPFS upload API tutorial.
Conclusion
Storing NFT metadata on IPFS ensures your digital assets remain accessible and valuable long-term. By following this guide, you’ve learned to:
- Structure ERC-721 compliant metadata
- Upload assets and metadata using Python and JavaScript
- Implement proper
tokenURIfunctions - Handle batch operations for large collections
- Apply best practices for production deployments
The combination of IPFS’s decentralized architecture and reliable pinning services creates the foundation for successful NFT projects that stand the test of time.
Ready to start pinning? Create a free account — 500 files, 1 GB storage, dedicated gateway. No credit card required.


