NFT Metadata Storage: The Complete Guide to IPFS for NFT Creators

Step-by-step guide to storing NFT metadata on IPFS using ERC-721 tokenURI patterns, with Python and JavaScript code examples for NFT developers.

Nacho Collby Updated: 9 min read
Step-by-step guide to storing NFT metadata on IPFS using ERC-721 tokenURI patterns, with Python and JavaScript code examples for NFT developers.
TL;DR
  • Store NFT metadata as immutable, content-addressed JSON so tokenURI links never go stale.
  • Upload the image asset first, then embed its CID inside the ERC-721 metadata JSON before pinning.
  • IPFS CIDs protect long-term NFT value even if the original hosting service disappears.
  • Cryptographic hashing guarantees NFT metadata integrity — any edit produces a different CID.

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.

IPFS Ninja

How IPFS Protects Your NFT Metadata: 4 Guarantees Centralised Hosts Cannot Make#

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:

  1. Main assets: Images, videos, or other primary content
  2. Metadata files: JSON files describing each NFT
  3. 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 backoff

3. 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 True

Choosing 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 tokenURI function 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:

  1. Regular Health Checks: Verify metadata and images remain accessible
  2. Backup Important CIDs: Keep records of all uploaded content identifiers
  3. Monitor Analytics: Track access patterns and storage usage
  4. 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 tokenURI functions
  • 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, 2 GB bandwidth/mo. No credit card required.

Nacho Coll

About the author

Founder & Engineer at IPFS.NINJA

Nacho founded IPFS.NINJA to make content-addressed storage feel as simple as an S3 PUT — a single API call, a permanent CID, no wallets or peer discovery to reason about. Writes about IPFS internals, decentralized storage patterns, and the pinning-service landscape from the operator side of the wire.

Back to Blog

Related Posts