Lesson 2: Build a Basic NFT

What are we buidling?

In this project we will build a basic NFT smart contract, that will serve us as the backbone or foundation for almost any future NFT project.

Why? Because NFTs are not just jpegs, they carry many superpowers that could improve our lives in many ways.

What is an NFT? NFT stands for Non-Fungible Token. It is basically a crypto token that is unique and cannot be replaced, because there is no other token that is identical in utility and therefore value.

So, what can we do with them? Since they live on the blockchain and are unique, we can use them to verify ownership of a real world asset (real estate deeds, art), to represent a membership (club membership, fan club), to track real world items in a supply chain (medicine, food, clothing), to use as tickets (movies, events, concerts, VIPs) and many more use cases we have yet to discover.

But, what is it with all the hype around them? As well as being unique, they have the ability to carry properties inside them. The value inherent to blockchains and the fact ownership can be transferred, leads to people attaching value to them. This has created huge opportunities for marketplaces to buy and sell these assets.

Now that we know they can be much more than just an image, what are we waiting for!

By the end of this lesson we are going to learn a lot of things. A simple breakdown of the steps to get there is:

Set up your environment

like the pros. For that we are going to set up our working environment in our computer, making sure we have all the necessary tools to create, debug and deploy smart contracts.


To begin our project, we should open a console (check your Operating System options on how to use one).

We will be using npm, the package manager from Node, and its command npx that allows us to run or execute a package or one of its scripts. If you are comfortable with another package manager, feel free to use it.

Let’s create and cd into our D_D Academy projects folder, and create a folder for our NFT project:

# create a folder for all our D_D Academy projects
mkdir d_d_academy
cd d_d_academy

# create a folder for this project
mkdir projectNFT
cd projectNFT

# initialize our folder as an npm package
npm init -y

# install hardhat (and its dependencies)
npm install --save-dev hardhat

The --save-dev flag used in the last command, lets the project know it's a development dependency (not needed in production). You can view what dependencies are needed in the package.json file in the root of the project.

Create your project

Once we have our environment set up, we need to create a project.


We run the npx hardhat command to create a basic project:

After you enter your npx hardhat command, what information can you see? What are those ten lines and what is?

// 888    888                      888 888               888
// 888    888                      888 888               888
// 888    888                      888 888               888
// 8888888888  8888b.  888d888 .d88888 88888b.   8888b.  888888
// 888    888     "88b 888P"  d88" 888 888 "88b     "88b 888
// 888    888 .d888888 888    888  888 888  888 .d888888 888
// 888    888 888  888 888    Y88b 888 888  888 888  888 Y88b.
// 888    888 "Y888888 888     "Y88888 888  888 "Y888888  "Y888

👷 Welcome to Hardhat v2.11.1 👷

? What do you want to do? … 
❯ Create a JavaScript project
  Create a TypeScript project
  Create an empty hardhat.config.js

We choose Create a Javascript project and hardhat will create an example project for us. It will give us 3 prompts for options. Choosing the defaults is ok for us. Here's what mine asked me:

✔ What do you want to do? · Create a JavaScript project
✔ Hardhat project root: · ~/d_d_academy/projectNFT
✔ Do you want to add a .gitignore? (Y/n) · y
✔ Do you want to install this sample project's dependencies with npm (@nomicfoundation/hardhat-toolbox)? (Y/n) · y

The project asked us to install @nomicfoundation/hardhat-toolbox in the last prompt. If they didn’t install or we accidentally chose ‘n’, we can always install them manually with:

npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox @nomicfoundation/hardhat-chai-matchers

We delete some files so we start fresh:

rm contracts/*.sol
rm scripts/*.js
rm test/*.js

In a Hardhat project, the default folders are supposed to be for:

default paths

We now need to add our last dependency (OpenZeppelin contracts):

npm install @openzeppelin/contracts

Open Zeppelin developed a lot of standard contracts that are super powerful, widely used and fully tested and audited.

Let’s start coding!

With our project folder ready, we can now fire up our Code Editor.


We are using VSCode for this one (we think it has a good balance of costs vs. benefits) but you can use whatever you feel comfortable with. A few of the more popular code editors are VSCode, Atom, Sublime, Vim, emacs, and many more.

Enough rambling, let’s start writing our contract!


In your console, type code . and yes, with that dot at the end! Wait for the magic.

Let’s create an empty file named ProjectNFT.sol inside the contracts/ folder and copy this code inside:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.12;

contract ProjectNFT {

If you’ve been through our first project, you’ll remember the first lines defines our copyright license, the second one defines the solidity version we are going to be using for this contract and the last two lines are how we declare a smart contract in solidity.

With an empty project, now we can start adding what we need to create our awesome NFT collection. In particular, ERC721 (the formal specification to follow for NFTs) have a lot of requirements to meet (they should have a numbered ID, functions to transfer them, approve others as delegates to transfer them, etc).

All of it is pretty much for our project, but luckily OpenZeppelin has developed a lot of contracts that implement these standards and more, they are widely used and audited and, the thing we love about them, they are open-source and we can use them to leverage our learning process. With that in our heads, we are going to use them inside our contract.

Standing in the shoulder of giants

For this we are going to ‘inherit’ (think of using the properties of or extending for now) the ERC721 contract from OpenZeppelin. In the same way you inherit the DNA and characteristics from your family/relatives.

This is what we need in our code:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.12;

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

contract ProjectNFT is ERC721 {

    constructor(string memory _name, string memory _symbol)
        ERC721(_name, _symbol) {}


Whose is whose?

We need to identify our NFTs, this is what gives each one its uniqueness on the blockchain.


We are going to use a state variable to keep track of the total supply of NFTs, which we can use as our ID for each new token created:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.12;

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

contract ProjectNFT is ERC721 {

    uint256 private totalSupply;

    constructor(string memory _name, string memory _symbol)
        ERC721(_name, _symbol) {}


We made our totalSupply variable private and it will store exactly how many NFTs have been minted so far. In the next step we will use this variable to identify each new NFT that’s created.

Mint… Feels Good

Once we have a way to identify our NFTs, we only need a way to create them.


We will add a function to mint new items, and yes, that means creating a new NFT:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.12;

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

contract ProjectNFT is ERC721 {

    uint256 private totalSupply;

    constructor(string memory _name, string memory _symbol)
        ERC721(_name, _symbol) {}

    function mint() public {
        _safeMint(msg.sender, totalSupply);


What our mint function does first is increment our totalSupply state variable by 1, because our collection will have one item more. Then it calls a function from our inherited ERC721 contract called _safeMint that allows a new item to be added to our ProjectNFT contract.

As you can see, OpenZeppelin implementation takes care of all the heavy lifting, but later in our learning journey we will be able to understand how it all works inside the hood.

Ok but, what does it do?

We can create unique NFTs by now, but we didn't add any media or utility to them just yet.

We are missing the last piece of the puzzle. For now they are just a numbered proof of ownership from our contract, stored on the blockchain. So here is where the magic starts.

We can make our NFT collection about anything we want: text, images, music, videos, anything you want. But how?

The ERC721 implementation we inherited has a function to store a tokenURI.

What is this black magic? This is just a unique URI, a web address if you prefer, that points to something on the internet, whatever we want it to be. e.g. a video, an image.

OpenZeppelin ERC721 contract provides us a way to create unique URIs from the IDs of the NFTs. It gives a way to define a base path for a web address and just attaches the token ID at the end of it. So, if you wanted, you could upload your NFT info (we'll look at how we store that later) to any web address, say and number each info file with the corresponding ID (1, 2, 3, 4…).

We are not going to limit the size of our collection for this project.

So, to let our inherited contract know where our NFTs will be stored, we have to override an OpenZeppelin function. Actually, you are expected to do this if you look into OpenZeppelin's documentation. There’s a good reason for everything. Well, lots of things. And this is one of them!

Here's the modifications we will add and I'll explain them below:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.12;

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

contract ProjectNFT is ERC721 {

  uint256 private totalSupply;
  string private ipfsBaseURI;

  constructor(string memory _name, string memory _symbol, string memory baseURI)
    ERC721(_name, _symbol) {
      ipfsBaseURI = baseURI;

  function mint() public {
    _safeMint(msg.sender, totalSupply);

  function _baseURI() internal view override returns (string memory) {
    return ipfsBaseURI;


What have we done?

Internally, when someone needs to consult an NFT's info, they call the OpenZeppelin's tokenURI function. That function only joins the return value of our newly _baseURIfunction with the ID of the token requested.

With that we can see the power of inheritance and overriding. We can inherit a whole set of functionalities and change only what we need to create a different experience.


Media for our NFTs

We have most of our smart contract ready, so we are going to focus on the media that we want on our NFTs.

We propose a "Choose your own adventure" for this part of the project:

Deploy! (back to Solidity, kind of)

You might remember this from the previous project, when we used Remix. Once we have our contract ready, we can compile and deploy. In remix, we had a bunch of buttons in the web interface, but now we are getting pro (a.k.a. doing it all ourselves, with the console).

Why did we deviate from Solidity? Because of the way we set our contract up. When we deploy we tell our contract where our files are stored. Therefore we had to store the files somewhere to get the address we need to run the deployment.

So, now that we have our media and JSONs uploaded, we can deploy our NFT contract!


Remember that scripts folder at the beginning of our project? That's where we are going to create our script to tell Hardhat the set of instructions to follow. We can create one for deploy, another one to mint or call functions of our contract. Hardhat uses javascript for this.

So, let's create a deploy.js under the scripts/ folder and copy this code:

// import Hardhat to use it inside our script
const hre = require('hardhat')

// We define a variable with the name of our contract
const CONTRACT_NAME = 'ProjectNFT'

// The 3 parameters for our smart contract's constructor (CHANGE THIS!!!)
const NFT_NAME = 'Academy'
const NFT_DESCRIPTION = 'D_D Academy Basic NFT Collection'

// Change this if you created your own images/JSONs:
const NFT_BASE_URI = ''

// We define a function with all we want Hardhat to run
async function main() {
  // We get the contract factory from Hardhat
  const ContractFactory = await hre.ethers.getContractFactory(CONTRACT_NAME)

  // We deploy the contract (notice we pass 3 parameters that our contract's constructor needs)
  const contract = await ContractFactory.deploy(

  // We wait for it to be deployed to the blockchain
  await contract.deployed()

  // We print the contract's address to the console
  console.log(`${CONTRACT_NAME} deployed to: ${contract.address}`)

  // --> ( We'll add more stuff here later ) <--

// We call the main function we created above (don't change this)
  .then(() => process.exit(0))
  .catch((error) => {

The code has comments outlining what each section does, but it basically has 1 variable for our contract's name so the script knows which contract to deploy, and 3 more variables for the data our constructor needs: a Name, a Description and a Base URI for our files. With that info set at the top, the script deploys the contract, waits for it to be deployed and then prints the address of our deployed contract to the console.

Before we run our script, we need to tell Hardhat what solidity version our contracts are using. For that, we need to go into the hardhat.config.js file in our root folder. Find the line that says solidity: '0.8.xx', and replace the 0.8.xx for the pragma used in our contract: 0.8.12.

We could have used a range of solidity versions e.g. ^0.8.0 in our contract, but we like to promote best practices by choosing a fixed solidity version and knowing beforehand nothing is going to change in our bytecode if a new version in that range is released.

Now, to run the script we type this in the console:

npx hardhat run scripts/deploy.js

This is the output i got:


Notice that ‘Compiled 10 Solidity files successfully’? But we only wrote 1 Solidity file!

Well, we are actually inheriting some of OpenZeppelin's implementations, so the compiler also has to process everything we are importing in our contract.

Since we are in a development environment, Hardhat provides us with the tools to run this in a local blockchain, without the hassle of dealing with blockchain times, tokens to pay gas for our deployment, etc. This means our contract was deployed to a dummy blockchain Hardhat created, it all went well, and then the blockchain gets deleted.

Taking advantage of this, we are going to add a mint after the deploy to test if all goes well.

Let's add these lines after the console.log part, right where we left a placeholder before in our deploy.js file:

	// --> ( We'll add more stuff here later ) <--

  // We call the contract to mint 1 NFT
  const tx = await

  // We wait for the transaction to finish
  await tx.wait()

  // We ask for the tokenURI of the recently minted NFT
  // (we assume 1 is the tokenId  since we only minted one)
  const tokenURI = await contract.tokenURI(1)

  // We print out the tokenURI
  console.log(`NFT 1 tokenURI: ${tokenURI}`)


Let's run the script again and see what it tells us! Here's my output:


We got the URI of the token, right? So, now we know we can deploy it (and mint) to a real blockchain!

Things just got real!

For us to get our contract out of our computers and into the real world, we are going to run the deploy again, but this time to a testnet.

What is a testnet? It is a basically a whole running blockchain, but it runs only so people can try stuff out. On it, you have eth, tokens, NFTs, but they have no monetary value. This way, you can develop and test your contracts without fear of losing real money. For the moment, we are choosing to do this on the Goerli (it's one of the many Ethereum testnets)

Before we go any further, let's take an extra step for precaution, until the next project where we are going to learn how to use collaborative tools to store our projects. But until then, let's open our main folder's .gitignore file an add this line anywhere:


In order to do this we need:

First and foremost, security. We are exploring new grounds, experimenting and by doing so, we could make mistakes that we don't want to happen where we have our money.

We strongly recommend you use a separate wallet for developing, completely independent from the one that holds your personal assets on the blockchain, if you have any.

Wallets have a special number, called a private key. You should never, ever share your private keys with anyone. If anyone has that number, they can take full control of the wallet, and empty it of all that is in it!


To be able to deploy our contract, we are going to use that number, that's why we recommend, yet again, to use a separate wallet for developing.

We need to inform Hardhat what node are we using to connect to the blockchain/network

Once you have your wallet funded with test eth, you need sign up for one of the Ethereum RPC Node Providers. Alchemy and Infura are the most used, Ankr has a "community endpoint" without signing up that is not dedicated, to list a few options.

Once you sign up, you'll be asked to create an App, be sure to select the Goerli network there.

When the app is created, you'll see a "View Key" button, or similar. Press it and copy the HTTP link, we'll use it in our next step.

Once you have funded your wallet with fake eth, we can go ahead and change our Hardhat configuration file. Go into your main project's folder and open hardhat.config.js.

We are going to replace our file with this:




 * @type import('hardhat/config').HardhatUserConfig
module.exports = {
  solidity: '0.8.12',
  networks: {
    goerli: {
      url: RPC_API_KEY,
      accounts: [WALLET_PRIVATE_KEY],

And you need to fill those 2 global variables with your own for hardhat to communicate correctly to the network.

We have already gone through how to get your API KEY.

For your wallet's private key, the most sensitive data of the project, you need to open Metamask, click on the three dots next to your Account Name, and then on Account Details, then click on Export Private Key. It will ask for your Metamask password (the one you use to open it, NOT your seed phrase). It also shows you a notice so you know that you are entering the danger zone. Confirm and you'll be able to copy your private key. Paste it in to our hardhat.config.js

Please, if you are already a developer and you plan to use git to store your project, don't store your hardhat.config.js on it, because you will have your private key there.

Ok. We are ready, let's deploy to the Goerli testnet!

Now we need to run our deploy.js script and the deployment is going to cost us some eth (test eth, don't worry) from our wallet, since we are storing information (i.e. our contract) in the blockchain. We only have to add 2 words to the command to let Hardhat know we are doing the real thing now. Remember our deploy script will create our contract AND mint one NFT. If you only want to deploy, delete all the lines we added to test the minting.


npx hardhat run scripts/deploy.js --network goerli

This is the output i got in my console:


Remember we are deploying to the actual blockchain, a testnet, but a blockchain nonetheless.

Now that we have run the deploy script, delete the private key and the RPC API key from hardhat.config.js to minimize the handling of the wallet. In the next project we will learn about tools to collaborate and store your projects online

So now we can go and explore the chain to find your contract (and mine too!). Just go to Goerli Etherscan (or Opensea) and search for the address of our deployed contract.

You can view my links as an example on Etherscan and Opensea


And that's it! You have created your own NFT collection from scratch! What a superstar!!


Developer DAO Foundation © 2023

Website content licensed under CC BY-NC 4.0.

Website code is licensed under MIT.

Privacy Policy