What data should be stored inside an NFT token?

I’m building an NFT marketplace with Solidity and using OpenZeppelin’s ERC-721 as the base contract. Right now my tokens have 5 properties: id, image hash, description, collection name, and creator address. I store the IPFS hash for the actual image file.

I’m confused about where to store this metadata. Currently I have an Asset struct with all these properties, put it in an array, then mint the NFT using the asset’s id and owner address. This means I’m keeping all the important data outside the actual ERC-721 contract.

This makes me wonder what the NFT actually represents if all the real information lives in my custom struct rather than in the token itself.

Is this the right approach? Does ERC-721 just provide the basic token functions while the actual metadata goes elsewhere?

Here’s my current implementation:

pragma solidity ^0.5.0;

import "./ERC721Full.sol";

contract ArtMarket is ERC721Full {
  string public contractName;
  Asset[] public tokens;
  uint public assetCounter = 0;
  mapping(uint => bool) public _tokenExists;
  mapping(uint => Asset) public assets;

  struct Asset {
    uint tokenId;
    string ipfsHash;
    string title;
    string category;
    address payable creator;
  }

  event AssetMinted(
    uint tokenId,
    string ipfsHash,
    string title,
    string category,
    address payable creator
  );

  constructor() public payable ERC721Full("ArtMarket", "ART") {
    contractName = "ArtMarket";
  }

  function createAsset(string memory _hash, string memory _title, string memory _category) public {
    require(bytes(_hash).length > 0);
    require(bytes(_title).length > 0);
    require(bytes(_category).length > 0);
    require(msg.sender != address(0));

    assetCounter++;

    assets[assetCounter] = Asset(assetCounter, _hash, _title, _category, msg.sender);

    require(!_tokenExists[assetCounter]);
    uint _tokenId = tokens.push(assets[assetCounter]);
    _mint(msg.sender, _tokenId);
    _tokenExists[assetCounter] = true;

    emit AssetMinted(assetCounter, _hash, _title, _category, msg.sender);
  }
}

Any advice on improving this code would be great. I’m new to Ethereum development so not sure if I’m doing this right.

Your approach is common, but there’s a problem with how you’re handling metadata. ERC-721 expects metadata through the tokenURI function - it should return a URI pointing to a JSON file with the token’s metadata. You’re storing everything on-chain in your struct. This works but gets expensive fast. Most projects store metadata on IPFS and only keep the IPFS hash in the contract. Your Asset struct should probably just have the tokenId and metadataURI. Put all the detailed stuff (title, category, image hash, etc.) in a JSON file on IPFS. This way the ERC-721 contract serves metadata through the standard interface while keeping gas costs reasonable. Also, you’re not implementing the tokenURI function. Without it, wallets and marketplaces can’t display your NFT metadata properly. The tokenURI should return something like https://ipfs.io/ipfs/YOUR_METADATA_HASH where the hash points to your JSON metadata file.

Interesting question! I’ve been dealing with similar stuff in my own NFT project.

Your setup seems to duplicate data - you’ve got the Asset struct AND you’re pushing to the tokens array. Why not just use the mapping and ditch the array?

More importantly, what’s your long-term plan? If you want these assets traded on OpenSea or other marketplaces, you’ll need that tokenURI function pointing to proper metadata JSON files. Right now you’re locked into your own marketplace.

What happens when your contract goes down or you stop maintaining it? All that struct metadata becomes inaccessible, which defeats the whole “permanent” NFT concept.

Ever consider a hybrid approach? Store essential stuff on-chain (like creator address for royalties) but move descriptions to IPFS? Lower gas costs plus decentralized storage.

Why keep everything in the contract instead of standard metadata? Avoiding IPFS completely or is there another benefit I’m missing?

your code’s solid for learning, but there’s a bug - you’re using array length as tokenId instead of your counter when minting. also, storing everything onchain gets expensive fast. most people just store the ipfs hash and handle the json offchain. saves a ton on gas.