What data does an NFT actually store?

I’m building an NFT marketplace with Solidity and using OpenZeppelin’s ERC-721 contract as a base. Right now my tokens have 5 properties (id, hash, description, collection, and creator) and I store the IPFS hash for images.

I’m confused about where to put all this data. I created an Asset struct with these properties, add it to an array, then mint using the asset ID and owner address. But this means I’m keeping all the real data outside the ERC-721 contract.

This makes me wonder what the NFT actually is, since the properties belong to my struct and the NFT just references it. Is this the right approach? Does ERC-721 just handle the basic token functions while metadata lives elsewhere?

Here’s my current setup:

pragma solidity ^0.5.0;

import "./ERC721Full.sol";

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

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

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

  constructor() public payable ERC721Full("ArtMarket", "ARTMKT") {
    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));

    assetCount++;

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

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

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

Any feedback on improving this code would be great. I’m new to Ethereum development so sorry if this seems basic.

This confusion happens all the time with NFTs! Your approach is fine - tons of projects do this.

NFTs don’t store actual data like images on-chain because it’d cost a fortune. They just store ownership proof and a pointer to where the real data lives (your IPFS hash).

Think of the ERC-721 contract as a certificate saying “this person owns token #123” with a URL or hash pointing to the asset. Keeping your Asset struct separate is totally normal.

Quick question about your code - why use both an array AND mapping for assets? The mapping should handle it alone. Also, you’re incrementing assetCount first then using it as ID, so your first token gets ID=1, not 0. Intentional?

One issue: tokens.push() returns the array’s new length, not the index. With 5 items, push returns 6 but your token ID might be 5. Check that logic.

Why store everything on-chain instead of using standard tokenURI pointing to JSON metadata? Gas costs an issue for you?

Your architecture looks solid for a beginner setup. ERC-721 is basically an ownership registry - it tracks who owns what token ID, handles transfers, and provides metadata functions. Having the actual asset data in your struct is totally normal. I’d suggest implementing tokenURI properly to return JSON metadata that follows OpenSea’s standards. Also, do you really need both the tokens array and assets mapping? They’re storing duplicate data. The mapping alone should work for most cases. Your on-chain vs off-chain balance makes sense. Storing everything on-chain would cost users way too much, and IPFS keeps it manageable. Just know this creates centralization risk if your IPFS nodes go down, but most projects accept that tradeoff.

yep, that’s how it goes. keeping metadata off-chain is way cheaper than putting it on-chain. your nft proves ownership while the struct or IPFS has the details. consider adding a tokenURI function, it’ll help with platforms like OpenSea.