What is IPFS?
IPFS (InterPlanetary File System) is a peer-to-peer distributed file system that makes the web more decentralized. Instead of addressing files by their location (like traditional URLs: https://server.com/file.png), IPFS addresses files by their content hash — a unique fingerprint derived from the file's data. This is called content addressing. If the content changes, the hash changes, guaranteeing data integrity. Any node that has the file can serve it.
Why IPFS for Web3?
- Immutability: Content hashes guarantee the data has not been tampered with
- Decentralization: No single point of failure — files are distributed across many nodes
- Permanence: As long as at least one node pins the data, it remains available
- Censorship resistance: No central authority can remove content from the network
- Deduplication: Identical files share the same hash, saving storage across the network
Content Identifiers (CIDs)
A CID is the unique address of content on IPFS. It encodes the hash algorithm used, the hash itself, and the codec (how to interpret the data). CIDv1 is the modern format: bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi. When you upload a file, IPFS computes its CID, and anyone with that CID can retrieve the exact same file from any IPFS node.
Using Pinata for IPFS Pinning
While any IPFS node can host files temporarily, pinning services like Pinata, Infura IPFS, or web3.storage ensure your files stay available permanently. They run dedicated IPFS nodes that pin your content.
// Install: npm install pinata-web3
import { PinataSDK } from 'pinata-web3';
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT!,
pinataGateway: 'your-gateway.mypinata.cloud',
});
// Upload a file
async function uploadFile(file: File) {
const result = await pinata.upload.file(file);
console.log('CID:', result.IpfsHash);
console.log('URL:', `https://your-gateway.mypinata.cloud/ipfs/${result.IpfsHash}`);
return result.IpfsHash;
}
// Upload JSON metadata (for NFTs)
async function uploadMetadata(tokenId: number, imageCID: string) {
const metadata = {
name: `My NFT #${tokenId}`,
description: 'A unique digital collectible',
image: `ipfs://${imageCID}`,
attributes: [
{ trait_type: 'Background', value: 'Blue' },
{ trait_type: 'Rarity', value: 'Rare' },
],
};
const result = await pinata.upload.json(metadata);
console.log('Metadata CID:', result.IpfsHash);
return result.IpfsHash;
}
// Upload an entire directory (for NFT collections)
async function uploadCollection(files: File[]) {
const result = await pinata.upload.fileArray(files);
// Result is a directory CID: ipfs://QmDir.../0.json, ipfs://QmDir.../1.json, etc.
console.log('Collection CID:', result.IpfsHash);
return result.IpfsHash;
}
Integrating IPFS with Smart Contracts
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
contract IPFSNft is ERC721, Ownable {
using Strings for uint256;
// Base URI pointing to IPFS directory
// e.g., "ipfs://QmCollectionHash/"
string public baseURI;
// For revealed/unrevealed mechanics
string public unrevealedURI;
bool public revealed = false;
uint256 private _nextTokenId;
constructor(string memory _unrevealedURI)
ERC721("IPFS NFT", "INFT")
Ownable(msg.sender)
{
unrevealedURI = _unrevealedURI;
}
function mint() external {
_safeMint(msg.sender, _nextTokenId++);
}
function tokenURI(uint256 tokenId)
public view override returns (string memory)
{
_requireOwned(tokenId);
if (!revealed) {
return unrevealedURI;
}
// Returns: ipfs://QmCollectionHash/42.json
return string(abi.encodePacked(baseURI, tokenId.toString(), ".json"));
}
function reveal(string memory _baseURI) external onlyOwner {
baseURI = _baseURI;
revealed = true;
}
}
Fetching IPFS Data in Your dApp
// Resolving IPFS URIs in the frontend
function ipfsToHttp(ipfsUri: string): string {
if (ipfsUri.startsWith('ipfs://')) {
const cid = ipfsUri.replace('ipfs://', '');
// Use a public or private IPFS gateway
return `https://cloudflare-ipfs.com/ipfs/${cid}`;
// Or your Pinata gateway:
// return `https://your-gateway.mypinata.cloud/ipfs/${cid}`;
}
return ipfsUri;
}
// Fetch NFT metadata and display it
async function fetchNFTMetadata(tokenURI: string) {
const httpUrl = ipfsToHttp(tokenURI);
const response = await fetch(httpUrl);
const metadata = await response.json();
return {
name: metadata.name,
description: metadata.description,
image: ipfsToHttp(metadata.image),
attributes: metadata.attributes,
};
}
// Usage in React component
function NFTCard({ tokenURI }: { tokenURI: string }) {
const [metadata, setMetadata] = useState<any>(null);
useEffect(() => {
fetchNFTMetadata(tokenURI).then(setMetadata);
}, [tokenURI]);
if (!metadata) return <div>Loading...</div>;
return (
<div className="border rounded-xl overflow-hidden">
<img src={metadata.image} alt={metadata.name} />
<div className="p-4">
<h3>{metadata.name}</h3>
<p>{metadata.description}</p>
</div>
</div>
);
}
Arweave: Permanent Storage Alternative
Arweave is another decentralized storage option that guarantees permanent data storage through a one-time payment model. Unlike IPFS where data persists only as long as someone pins it, Arweave uses an economic mechanism (endowment) to pay for storage in perpetuity. Many high-value NFT projects use Arweave for metadata and media storage.
Storage Decision Guide
- On-chain storage: Most expensive. Only for tiny data (<1KB). Fully decentralized and permanent.
- IPFS + Pinning: Good balance of cost and decentralization. Requires active pinning. Best for most NFT projects.
- Arweave: One-time payment for permanent storage. Great for critical data that must last forever.
- Centralized servers: Cheapest but defeats the purpose of decentralization. Acceptable for non-critical data.