ethereum/nft-tech-stack

NFT Tech Stack Complete Guide: Storage + Metadata + Minting

ethereumguide👥 Communityconfidence highhealth 100%
v1.0.0·Updated 3/20/2026

Storage Solutions Comparison

SolutionCostPermanenceDecentralizedRecommended For
IPFS + PinataLow ($0/mo free tier)Depends on pin serviceYesEarly-stage projects, fast launch
ArweaveOne-time fee (~$0.01/MB)PermanentYesHigh-value NFTs, long-term projects
FilecoinPay per time periodDuration of contractYesLarge file storage
Centralized ServerVery lowNot guaranteedNoNot recommended

Metadata Standard (OpenSea Compatible)

{
  "name": "My NFT #1",
  "description": "This is the first NFT in the collection.",
  "image": "ipfs://QmXxx.../1.png",
  "external_url": "https://myproject.com/nft/1",
  "attributes": [
    { "trait_type": "Background", "value": "Blue" },
    { "trait_type": "Eyes", "value": "Laser", "rarity": 0.02 },
    { "trait_type": "Level", "value": 5, "display_type": "number" },
    { "trait_type": "Birthday", "value": 1546360800, "display_type": "date" }
  ],
  "animation_url": "ipfs://QmXxx.../1.mp4"  // optional, video/audio
}

tokenURI formats:

ipfs://CID/1.json       ← recommended (fully decentralized)
https://api.xxx.com/1   ← works, but depends on a server
ar://TXID/1.json        ← Arweave (permanent)

Pinata (Simplest IPFS Option)

import PinataSDK from "@pinata/sdk"
const pinata = new PinataSDK(process.env.PINATA_API_KEY, process.env.PINATA_SECRET)

// Upload image
const imgResult = await pinata.pinFileToIPFS(fs.createReadStream("./nft.png"), {
  pinataMetadata: { name: "NFT #1" }
})
const imageURI = `ipfs://${imgResult.IpfsHash}`

// Upload metadata JSON
const metadata = { name: "My NFT #1", image: imageURI, attributes: [...] }
const metaResult = await pinata.pinJSONToIPFS(metadata)
const tokenURI = `ipfs://${metaResult.IpfsHash}`

Arweave (Permanent Storage)

import Arweave from "arweave"
const arweave = Arweave.init({ host: "arweave.net", port: 443, protocol: "https" })

const key = JSON.parse(fs.readFileSync("arweave-key.json"))

// Upload file
const tx = await arweave.createTransaction({ data: fs.readFileSync("./nft.png") }, key)
tx.addTag("Content-Type", "image/png")
await arweave.transactions.sign(tx, key)
await arweave.transactions.post(tx)
const imageURI = `ar://${tx.id}`

// Or use Bundlr (cheaper for bulk uploads)
import { NodeBundlr } from "@bundlr-network/client"
const bundlr = new NodeBundlr("https://node1.bundlr.network", "arweave", key)
const response = await bundlr.uploadFile("./nft.png")

ERC-721 Minting Contract

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract MyNFT is ERC721URIStorage, Ownable {
  using Counters for Counters.Counter;
  Counters.Counter private _tokenIds;
  
  uint256 public constant MAX_SUPPLY = 10000;
  uint256 public constant PRICE = 0.05 ether;
  bool public saleActive = false;
  
  string private _baseTokenURI;  // ipfs://CID/
  
  constructor() ERC721("My NFT", "MNFT") Ownable(msg.sender) {}
  
  // Public mint
  function mint(uint256 quantity) external payable {
    require(saleActive, "Sale not active");
    require(quantity <= 10, "Max 10 per tx");
    require(_tokenIds.current() + quantity <= MAX_SUPPLY, "Exceeds supply");
    require(msg.value >= PRICE * quantity, "Insufficient payment");
    
    for (uint i = 0; i < quantity; i++) {
      _tokenIds.increment();
      _safeMint(msg.sender, _tokenIds.current());
    }
  }
  
  // Reveal (mint first, reveal metadata later)
  function setBaseURI(string memory baseURI) external onlyOwner {
    _baseTokenURI = baseURI;
  }
  
  function _baseURI() internal view override returns (string memory) {
    return _baseTokenURI;
  }
  
  // EIP-2981 royalties (5%)
  function royaltyInfo(uint256, uint256 salePrice) external view 
    returns (address receiver, uint256 royaltyAmount) {
    return (owner(), salePrice * 500 / 10000);
  }
  
  function withdraw() external onlyOwner {
    payable(owner()).transfer(address(this).balance);
  }
}

ERC-1155 (Multi-Token NFT / SFT)

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";

contract GameItems is ERC1155 {
  uint256 public constant SWORD = 1;
  uint256 public constant SHIELD = 2;
  uint256 public constant GOLD = 3;  // FT (fungible token)
  
  constructor() ERC1155("https://api.game.com/items/{id}.json") {
    _mint(msg.sender, SWORD, 100, "");   // 100 swords
    _mint(msg.sender, GOLD, 1e18, "");  // large supply of gold coins
  }
}

Lazy Minting (Gas Savings)

How it works: NFTs are not pre-minted; they are minted at the moment of purchase, with a signature used as the voucher.

// Seller signs a voucher offline
bytes32 hash = keccak256(abi.encode(tokenId, price, uri));
bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
(v, r, s) = vm.sign(sellerKey, ethHash);

// Buyer verifies on-chain and mints
function redeem(uint tokenId, uint price, string memory uri, bytes memory sig) external payable {
  require(msg.value >= price);
  address signer = ECDSA.recover(hash, sig);
  require(signer == seller, "Invalid signature");
  _safeMint(msg.sender, tokenId);
  _setTokenURI(tokenId, uri);
}
  • Manifold Studio — no-code NFT deployment
  • ThirdWeb — NFT SDK + dashboard
  • Zora Protocol — free mint + royalty infrastructure

NFT Tech Stack Complete Guide

Storage Solutions Comparison

SolutionCostPermanenceDecentralizedRecommended For
IPFS + PinataLow ($0/mo free tier)Depends on pin serviceYesEarly-stage projects, fast launch
ArweaveOne-time fee (~$0.01/MB)PermanentYesHigh-value NFTs, long-term projects
FilecoinPay per time periodDuration of contractYesLarge file storage
Centralized ServerVery lowNot guaranteedNoNot recommended

Metadata Standard (OpenSea Compatible)

{
  "name": "My NFT #1",
  "description": "This is the first NFT in the collection.",
  "image": "ipfs://QmXxx.../1.png",
  "external_url": "https://myproject.com/nft/1",
  "attributes": [
    { "trait_type": "Background", "value": "Blue" },
    { "trait_type": "Eyes", "value": "Laser", "rarity": 0.02 },
    { "trait_type": "Level", "value": 5, "display_type": "number" },
    { "trait_type": "Birthday", "value": 1546360800, "display_type": "date" }
  ],
  "animation_url": "ipfs://QmXxx.../1.mp4"  // optional, video/audio
}

tokenURI formats:

ipfs://CID/1.json       ← recommended (fully decentralized)
https://api.xxx.com/1   ← works, but depends on a server
ar://TXID/1.json        ← Arweave (permanent)

Pinata (Simplest IPFS Option)

import PinataSDK from "@pinata/sdk"
const pinata = new PinataSDK(process.env.PINATA_API_KEY, process.env.PINATA_SECRET)

// Upload image
const imgResult = await pinata.pinFileToIPFS(fs.createReadStream("./nft.png"), {
  pinataMetadata: { name: "NFT #1" }
})
const imageURI = `ipfs://${imgResult.IpfsHash}`

// Upload metadata JSON
const metadata = { name: "My NFT #1", image: imageURI, attributes: [...] }
const metaResult = await pinata.pinJSONToIPFS(metadata)
const tokenURI = `ipfs://${metaResult.IpfsHash}`

Arweave (Permanent Storage)

import Arweave from "arweave"
const arweave = Arweave.init({ host: "arweave.net", port: 443, protocol: "https" })

const key = JSON.parse(fs.readFileSync("arweave-key.json"))

// Upload file
const tx = await arweave.createTransaction({ data: fs.readFileSync("./nft.png") }, key)
tx.addTag("Content-Type", "image/png")
await arweave.transactions.sign(tx, key)
await arweave.transactions.post(tx)
const imageURI = `ar://${tx.id}`

// Or use Bundlr (cheaper for bulk uploads)
import { NodeBundlr } from "@bundlr-network/client"
const bundlr = new NodeBundlr("https://node1.bundlr.network", "arweave", key)
const response = await bundlr.uploadFile("./nft.png")

ERC-721 Minting Contract

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract MyNFT is ERC721URIStorage, Ownable {
  using Counters for Counters.Counter;
  Counters.Counter private _tokenIds;
  
  uint256 public constant MAX_SUPPLY = 10000;
  uint256 public constant PRICE = 0.05 ether;
  bool public saleActive = false;
  
  string private _baseTokenURI;  // ipfs://CID/
  
  constructor() ERC721("My NFT", "MNFT") Ownable(msg.sender) {}
  
  // Public mint
  function mint(uint256 quantity) external payable {
    require(saleActive, "Sale not active");
    require(quantity <= 10, "Max 10 per tx");
    require(_tokenIds.current() + quantity <= MAX_SUPPLY, "Exceeds supply");
    require(msg.value >= PRICE * quantity, "Insufficient payment");
    
    for (uint i = 0; i < quantity; i++) {
      _tokenIds.increment();
      _safeMint(msg.sender, _tokenIds.current());
    }
  }
  
  // Reveal (mint first, reveal metadata later)
  function setBaseURI(string memory baseURI) external onlyOwner {
    _baseTokenURI = baseURI;
  }
  
  function _baseURI() internal view override returns (string memory) {
    return _baseTokenURI;
  }
  
  // EIP-2981 royalties (5%)
  function royaltyInfo(uint256, uint256 salePrice) external view 
    returns (address receiver, uint256 royaltyAmount) {
    return (owner(), salePrice * 500 / 10000);
  }
  
  function withdraw() external onlyOwner {
    payable(owner()).transfer(address(this).balance);
  }
}

ERC-1155 (Multi-Token NFT / SFT)

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";

contract GameItems is ERC1155 {
  uint256 public constant SWORD = 1;
  uint256 public constant SHIELD = 2;
  uint256 public constant GOLD = 3;  // FT (fungible token)
  
  constructor() ERC1155("https://api.game.com/items/{id}.json") {
    _mint(msg.sender, SWORD, 100, "");   // 100 swords
    _mint(msg.sender, GOLD, 1e18, "");  // large supply of gold coins
  }
}

Lazy Minting (Gas Savings)

How it works: NFTs are not pre-minted; they are minted at the moment of purchase, with a signature used as the voucher.

// Seller signs a voucher offline
bytes32 hash = keccak256(abi.encode(tokenId, price, uri));
bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
(v, r, s) = vm.sign(sellerKey, ethHash);

// Buyer verifies on-chain and mints
function redeem(uint tokenId, uint price, string memory uri, bytes memory sig) external payable {
  require(msg.value >= price);
  address signer = ECDSA.recover(hash, sig);
  require(signer == seller, "Invalid signature");
  _safeMint(msg.sender, tokenId);
  _setTokenURI(tokenId, uri);
}
  • Manifold Studio — no-code NFT deployment
  • ThirdWeb — NFT SDK + dashboard
  • Zora Protocol — free mint + royalty infrastructure
  • OpenSea SDK — listing/trading integration