DEV Community

Ishan Maity
Ishan Maity

Posted on

Building an NFT Marketplace From Scratch: A Developer's Guide With Real Code

If you've ever wondered what actually happens behind the "Mint" or "Buy Now" button on an NFT marketplace, you're in the right place. Most explainers stop at buzzwords like "blockchain" and "decentralized," but never actually show you the code. This one won't do that.

By the end of this post, you'll understand the core architecture behind an NFT marketplace, see working Solidity and JavaScript code for the three most important pieces, and walk away with a real mental model of what any NFT marketplace development company is actually building when a client says "I want an OpenSea clone."

This is a long one, grab a coffee. We're going to build understanding piece by piece, not just skim the surface.

The Architecture, in Plain English First

Before touching any code, it helps to understand the four moving parts of any NFT marketplace:

The NFT smart contract (usually ERC-721 or ERC-1155) that mints and owns the tokens.
The marketplace smart contract that handles listing, buying, and fee logic.
Off-chain storage (usually IPFS) for the actual image, video, or metadata, since storing large files directly on-chain is prohibitively expensive.
The frontend, which talks to the blockchain through a library like ethers.js or web3.js, and talks to IPFS for metadata.

User uploads image and metadata
        |
        v
Metadata + image get pinned to IPFS
        |
        v
Smart contract mints the NFT, storing the IPFS URI on-chain
        |
        v
User calls the marketplace contract to list the NFT for sale
        |
        v
Another user calls "buyNFT," sending ETH and receiving the token
Enter fullscreen mode Exit fullscreen mode

Simple in concept, but the details matter a lot. Let's build each piece.

Part 1: The NFT Smart Contract (ERC-721)

Most NFT marketplaces are built on the ERC-721 standard, which represents unique, non-fungible tokens. Here's a minimal but functional minting contract using OpenZeppelin's battle-tested base contracts:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

contract MyNFT is ERC721URIStorage, Ownable {
    uint256 private _nextTokenId;

    constructor() ERC721("MyMarketplaceNFT", "MMNFT") Ownable(msg.sender) {}

    function mintNFT(address recipient, string memory tokenURI)
        public
        returns (uint256)
    {
        uint256 tokenId = _nextTokenId;
        _nextTokenId++;

        _safeMint(recipient, tokenId);
        _setTokenURI(tokenId, tokenURI);

        return tokenId;
    }
}
Enter fullscreen mode Exit fullscreen mode

A few things worth pointing out here for anyone new to Solidity:

_safeMint creates the token and assigns ownership, while also checking that the recipient can actually receive ERC-721 tokens (important if the recipient is a contract, not a wallet).
tokenURI is a link, usually pointing to IPFS, that contains the NFT's metadata: name, description, image link, and attributes.
Inheriting from OpenZeppelin's contracts instead of writing this from scratch isn't cutting corners, it's the industry standard, since these contracts have been audited extensively.
Part 2: The Marketplace Contract (Listing and Buying)

Minting is only half the picture. The marketplace contract is what actually lets people list NFTs for sale and buy them. Here's a simplified but functional version:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract NFTMarketplace is ReentrancyGuard {
    struct Listing {
        address seller;
        uint256 price;
        bool active;
    }

    // nftContract address => tokenId => Listing
    mapping(address => mapping(uint256 => Listing)) public listings;

    event NFTListed(address indexed nftContract, uint256 indexed tokenId, address seller, uint256 price);
    event NFTSold(address indexed nftContract, uint256 indexed tokenId, address buyer, uint256 price);

    function listNFT(address nftContract, uint256 tokenId, uint256 price) external {
        require(price > 0, "Price must be greater than zero");
        require(IERC721(nftContract).ownerOf(tokenId) == msg.sender, "You do not own this NFT");

        listings[nftContract][tokenId] = Listing(msg.sender, price, true);

        emit NFTListed(nftContract, tokenId, msg.sender, price);
    }

    function buyNFT(address nftContract, uint256 tokenId) external payable nonReentrant {
        Listing memory item = listings[nftContract][tokenId];

        require(item.active, "This NFT is not listed for sale");
        require(msg.value >= item.price, "Insufficient payment");

        listings[nftContract][tokenId].active = false;

        IERC721(nftContract).transferFrom(item.seller, msg.sender, tokenId);
        payable(item.seller).transfer(msg.value);

        emit NFTSold(nftContract, tokenId, msg.sender, item.price);
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice the nonReentrant modifier on buyNFT. This isn't decoration, it's a direct defense against reentrancy attacks, the exact vulnerability that has drained millions of dollars from poorly written contracts. Whenever a contract sends funds, that's the moment an attacker looks for a way to sneak back in before state updates finish.

Also worth noting: in a production marketplace, you'd add platform fee logic, royalty payments to original creators (via EIP-2981), and approval checks (isApprovedForAll or getApproved) before allowing a transfer. This is a teaching example, not a production-ready contract.

Part 3: Connecting the Frontend With ethers.js

Smart contracts don't do you much good if users can't interact with them. Here's how a frontend would call the buyNFT function using ethers.js:

import { ethers } from "ethers";
import marketplaceABI from "./MarketplaceABI.json";

const marketplaceAddress = "0xYourMarketplaceContractAddress";

async function buyNFT(nftContractAddress, tokenId, priceInEth) {
  if (!window.ethereum) {
    alert("Please install MetaMask to continue.");
    return;
  }

  const provider = new ethers.BrowserProvider(window.ethereum);
  const signer = await provider.getSigner();

  const marketplace = new ethers.Contract(
    marketplaceAddress,
    marketplaceABI,
    signer
  );

  try {
    const tx = await marketplace.buyNFT(nftContractAddress, tokenId, {
      value: ethers.parseEther(priceInEth.toString()),
    });

    console.log("Transaction submitted:", tx.hash);
    await tx.wait();
    console.log("Purchase confirmed!");
  } catch (error) {
    console.error("Purchase failed:", error);
  }
}
Enter fullscreen mode Exit fullscreen mode

A quick walkthrough of what's happening:

BrowserProvider connects to the user's wallet (MetaMask, in most cases).
getSigner() gets permission to sign transactions on the user's behalf.
ethers.parseEther() converts a human-readable ETH amount (like "0.5") into the wei value the blockchain actually expects.
tx.wait() pauses execution until the transaction is confirmed on-chain, which is important since blockchain transactions aren't instant.

This same pattern, connect wallet, build contract instance, call function, wait for confirmation, is the backbone of almost every Web3 frontend interaction you'll ever write.

Q&A: The Questions Every Developer Asks When Building Their First NFT Marketplace

Q: Why store metadata on IPFS instead of directly on the blockchain? A: Blockchain storage is extremely expensive. Storing an image directly on-chain could cost thousands of dollars in gas fees for a single file. IPFS stores the actual content off-chain, while the smart contract only stores a small reference link (the URI), keeping costs manageable.

Q: What's the difference between ERC-721 and ERC-1155?

A: ERC-721 represents one unique token per contract call, ideal for one-of-a-kind art. ERC-1155 allows a single contract to manage multiple token types, including both unique and interchangeable (fungible) tokens, which is more gas-efficient for projects like game items or collections with multiple copies of the same asset.

Q: Do I need to build my own marketplace contract, or can I just use an existing protocol?

A: Both approaches are common. Building custom gives full control over fees, royalties, and features, but requires careful auditing. Using an existing, audited protocol as a base reduces security risk but limits customization. Most serious projects end up somewhere in between, using audited base contracts and customizing carefully on top.

Q: What happens if two people try to buy the same NFT at the same time?

A: This is exactly why the require(item.active, ...) check exists in the buyNFT function above. Blockchain transactions are processed one at a time within a block, so whichever transaction gets mined first succeeds, and the listing is marked inactive before the second transaction can execute, causing it to fail safely instead of allowing a double sale.

Q: How are creator royalties usually handled?

A: Most modern implementations use EIP-2981, a standard that lets an NFT contract specify a royalty percentage and recipient. Marketplaces that respect this standard automatically send a cut of every resale back to the original creator, though enforcement varies since it ultimately depends on the marketplace contract choosing to honor it.

Where This Gets More Complex (and Why Teams Bring in Specialists)

Everything above is a functional, teachable foundation, but a real production marketplace adds significant complexity on top:

Auction-style listings with time-based bidding logic
Batch minting and lazy minting (minting only when an NFT actually sells, to save gas)
Cross-chain compatibility

Off-chain indexing (using something like The Graph) so the frontend doesn't have to query the blockchain directly for every listing
Fraud prevention, wash trading detection, and marketplace-level moderation tools

This is usually the point where teams either invest heavily in specialized in-house blockchain engineers or bring in a dedicated NFT marketplace development company to handle the parts that go far beyond a
tutorial-level build: gas optimization, multi-audit security review, and scalable backend architecture that can handle thousands of concurrent listings without breaking.

It's also worth understanding what full nft marketplace development services typically include before scoping a project, since offerings vary a lot between vendors. Some cover only smart contract development.

Others include the full stack: smart contracts, frontend, backend indexing, wallet integration, IPFS pinning infrastructure, and post-launch security monitoring. Knowing which one you're actually buying makes a big difference in your final budget and timeline.

Teams like Dev Technosys, among others working in this space, typically fold these production-grade concerns into the build from day one rather than treating them as later add-ons, which tends to matter a lot once a marketplace has real users and real money moving through it.

Wrapping Up

Building an NFT marketplace isn't magic, and it isn't as simple as connecting a wallet button either.

It's a genuinely interesting intersection of smart contract engineering, off-chain infrastructure, and frontend development, all of which have to work together correctly, because unlike a typical web app, a smart contract bug can't always be quietly patched after launch.

If you made it this far, try extending the code above. Add a royalty split to the buy.

NFT function, or add an updateListingPrice function to the marketplace contract.

The best way to actually understand this stack is to break it, then fix it yourself.

What would you add to this marketplace contract first, auctions, royalties, or something else? Let me know in the comments, always curious what other devs prioritize.

Top comments (0)