Skip to main content

How to Write & Deploy an NFT (Part 1/3 of NFT Tutorial Series)

ERC-721
Alchemy
Solidity
smart contracts
Beginner
Sumi Mudgil
April 22, 2021
15 minute read

With NFTs bringing blockchain into the public eye, now is an excellent opportunity to understand the hype yourself by publishing your own NFT contract (ERC-721 Token) on the Ethereum blockchain!

In this tutorial, we will walk through creating and deploying an ERC-721 smart contract on the Sepolia test network using MetaMask (opens in a new tab), Solidity (opens in a new tab), Hardhat (opens in a new tab), Pinata (opens in a new tab) and Alchemy (opens in a new tab) (don’t fret if you don’t understand what any of this means yet — we will explain it!).

In Part 2 of this tutorial we’ll go through how we can use our smart contract to mint an NFT, and in Part 3 we’ll explain how to view your NFT on MetaMask.

And of course, if you have questions at any point, don’t hesitate to reach out in the Alchemy Discord (opens in a new tab) or visit Alchemy's NFT API docs (opens in a new tab)!

Step 1: Connect to the Ethereum network

There are a bunch of ways to make requests to the Ethereum blockchain, but to make things easy, we’ll use a free account on Alchemy (opens in a new tab), a blockchain developer platform and API that allows us to communicate with the Ethereum chain without having to run our own nodes.

In this tutorial, we’ll also take advantage of Alchemy’s developer tools for monitoring and analytics to understand what’s going on under the hood in our smart contract deployment. If you don’t already have an Alchemy account, you can sign up for free here (opens in a new tab).

Step 2: Create your app (and API key)

Once you’ve created an Alchemy account, you can generate an API key by creating an app. This will allow us to make requests to the Sepolia test network. Check out this guide (opens in a new tab) if you’re curious to learn more about test networks.

  1. Navigate to the "Create new app" page in your Alchemy Dashboard by selecting "Select an app" in the nav bar and clicking "Create new app"

Create your app

  1. Name your app (we chose “My First NFT!”), offer a short description, and choose a use case. Next, search for "Ethereum" and select the network. We’ll use Sepolia, the recommended testnet for application development (see Ethereum networks).

  2. Click "Next" to proceed, then “Create app” and that’s it! Your app should appear in the nav bar dropdown menu, with an API Key available to copy.

Step 3: Create an Ethereum account (address)

We need an Ethereum account to send and receive transactions. For this tutorial, we’ll use MetaMask, a virtual wallet in the browser used to manage your Ethereum account address. If you want to understand more about how transactions on Ethereum work, check out this page from the Ethereum foundation.

You can download and create a MetaMask account for free here (opens in a new tab). When you are creating an account, or if you already have an account, make sure to switch over to the "Sepolia" test network using the network selection menu (so that we’re not dealing with real money). If you do not see Sepolia listed, open the menu in the top right corner, select "Networks", and toggle "Show test networks" on (see MetaMask's guide to viewing testnets (opens in a new tab)).

Set Sepolia as your network

Step 4: Add ether from a Faucet

In order to deploy our smart contract to the test network, we’ll need some fake ETH. To get Sepolia ETH, pick one of the faucets listed in the Sepolia network details and enter your account address. If one doesn't work, try another as they can sometimes run dry, and some have eligibility rules: for example, the Alchemy Sepolia faucet (opens in a new tab) requires the receiving address to hold at least 0.001 ETH on Ethereum Mainnet. You should see ETH in your MetaMask account soon after!

Step 5: Check your Balance

To double check our balance is there, let’s make an eth_getBalance (opens in a new tab) request using Alchemy’s sandbox tool (opens in a new tab). This will return the amount of ETH in our wallet. After you input your MetaMask account address and click “Send Request”, you should see a response like this:

{ "jsonrpc": "2.0", "id": 0, "result": "0xde0b6b3a7640000" }

Note This result is in wei, not ETH. Wei is used as the smallest denomination of ether. The conversion from wei to ETH is 1 eth = 1018 wei. So if we convert 0xde0b6b3a7640000 to decimal we get 1*1018 wei, which equals 1 ETH.

Phew! Our fake money is all there.

Step 6: Initialize our project

First, we’ll need to create a folder for our project. Navigate to your command line and type:

mkdir my-nft
cd my-nft

Now that we’re inside our project folder, we’ll use npm init to initialize the project. If you don’t already have npm installed, follow the Node.js installation instructions (opens in a new tab) (we’ll need Node.js and npm for this tutorial).

npm init

It doesn’t really matter how you answer the installation questions; here is how we did it for reference:

Approve the package.json. Hardhat 3, which we install next, only supports ES module projects, so mark the project as one:

npm pkg set type=module

And we’re good to go!

Step 7: Install Hardhat (opens in a new tab)

Hardhat is a development environment to compile, deploy, test, and debug your Ethereum software. It helps developers when building smart contracts and dapps locally before deploying to the live chain.

Hardhat 3 requires Node.js v22.13.0 or later. Inside our my-nft project run:

npm install --save-dev hardhat

Check out this page for more details on installation instructions (opens in a new tab).

Step 8: Create Hardhat project

Running npx hardhat --init would generate a complete sample project, but for this tutorial we’ll set things up by hand so you can see every piece. In the root of your project folder, create a file called hardhat.config.js with the following contents:

import { defineConfig } from "hardhat/config"

export default defineConfig({
  solidity: "0.8.34",
})

This is where we’ll specify all of the set up for our project (on step 13). For now, it just tells Hardhat which version of the Solidity compiler to use.

Step 9: Add project folders

To keep our project organized, we’ll create two new folders. Navigate to the root directory of your project in your command line and type:

mkdir contracts
mkdir scripts
  • contracts/ is where we’ll keep our NFT smart contract code

  • scripts/ is where we’ll keep scripts to deploy and interact with our smart contract

Step 10: Write our contract

Now that our environment is set up, on to more exciting stuff: writing our smart contract code!

Open up the my-nft project in your favorite editor (we like VSCode (opens in a new tab)). Smart contracts are written in a language called Solidity which is what we will use to write our MyNFT.sol smart contract.‌

  1. Navigate to the contracts folder and create a new file called MyNFT.sol

  2. Below is our NFT smart contract code, which we based on the OpenZeppelin (opens in a new tab) library’s ERC-721 implementation. Copy and paste the contents below into your MyNFT.sol file.

  3. Because we are inheriting classes from the OpenZeppelin contracts library, in your command line run npm install @openzeppelin/contracts to install the library into our folder. This tutorial uses OpenZeppelin Contracts 5.x, which requires Solidity 0.8.24 or later.

So, what does this code do exactly? Let’s break it down, line-by-line.

At the top of our smart contract, we import three OpenZeppelin (opens in a new tab) smart contract classes:

  • @openzeppelin/contracts/token/ERC721/ERC721.sol contains the implementation of the ERC-721 standard, which our NFT smart contract will inherit. (To be a valid NFT, your smart contract must implement all the methods of the ERC-721 standard.) To learn more about the inherited ERC-721 functions, check out the interface definition here (opens in a new tab).

  • @openzeppelin/contracts/access/Ownable.sol sets up access control (opens in a new tab) on our smart contract, so only the owner of the smart contract (you) can mint NFTs. The owner is the initialOwner address passed to the constructor. (Note, including access control is entirely a preference. If you'd like anyone to be able to mint an NFT using your smart contract, remove Ownable and Ownable(initialOwner) from the contract definition and constructor, and onlyOwner from the mintNFT function.)

  • @openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol extends ERC-721 so that each token can store its own metadata URI, which we set when minting.

After our import statements, we have our custom NFT smart contract, which is surprisingly short — it only contains a counter, a constructor, and single function! This is thanks to our inherited OpenZeppelin contracts, which implement most of the methods we need to create an NFT, such as ownerOf which returns the owner of the NFT, and transferFrom, which transfers ownership of the NFT from one account to another.

The counter, _nextTokenId, keeps track of the total number of NFTs minted and sets the unique ID on our new NFT. (Each NFT minted using a smart contract must be assigned a unique ID—here our unique ID is just determined by the total number of NFTs in existence. ++_nextTokenId increases the counter before it is used, so the first NFT we mint with our smart contract has an ID of "1," our second NFT has an ID of "2," etc.)

In our ERC-721 constructor, you’ll notice we pass 2 strings, “MyNFT” and “NFT.” The first variable is the smart contract’s name, and the second is its symbol. You can name each of these variables whatever you wish!

Finally, we have our function mintNFT(address recipient, string memory tokenURI) that allows us to mint an NFT! You'll notice this function takes in two variables:

  • address recipient specifies the address that will receive your freshly minted NFT

  • string memory tokenURI is a string that should resolve to a JSON document that describes the NFT's metadata. An NFT's metadata is really what brings it to life, allowing it to have configurable properties, such as a name, description, image, and other attributes. In part 2 of this tutorial, we will describe how to configure this metadata.

mintNFT calls some methods from the inherited ERC-721 library, and ultimately returns a number that represents the ID of the freshly minted NFT.

Step 11: Connect MetaMask & Alchemy to your project

Now that we’ve created a MetaMask wallet, Alchemy account, and written our smart contract, it’s time to connect the three.

Every transaction sent from your virtual wallet requires a signature using your unique private key. To provide our program with this permission, we can safely store our private key (and Alchemy API key) in an environment file.

To learn more about sending transactions, check out the transactions documentation.

Create a .env file in the root directory of our project, and add your MetaMask private key and HTTP Alchemy API URL to it.

Your .env should now look like this:

API_URL="https://eth-sepolia.g.alchemy.com/v2/your-api-key"
PRIVATE_KEY="your-metamask-private-key"

To actually connect these to our code, we’ll reference these variables in our hardhat.config.js file in step 13. Node.js can read .env files on its own, so there's no extra package to install.

Don't commit .env! Please make sure never to share or expose your .env file with anyone, as you are compromising your secrets in doing so. If you are using version control, add your .env to a gitignore (opens in a new tab) file.

Step 12: Install Ethers.js

Ethers.js is a library that makes it easier to interact and make requests to Ethereum by wrapping standard JSON-RPC methods with more user friendly methods.

Hardhat makes it super easy to integrate Plugins (opens in a new tab) for additional tooling and extended functionality. We’ll be taking advantage of the Ethers plugin (opens in a new tab) for contract deployment (Ethers.js (opens in a new tab) has some super clean contract deployment methods).

In your project directory type:

npm install --save-dev @nomicfoundation/hardhat-ethers ethers

We’ll also add the plugin to our hardhat.config.js in the next step.

Step 13: Update hardhat.config.js

We’ve added several dependencies and plugins so far, now we need to update hardhat.config.js so that our project knows about all of them.

Update your hardhat.config.js to look like this:

configVariable tells Hardhat to read each value from an environment variable only when it's needed, so commands like compile work even before the values are set. If you'd rather not keep your private key in a plain text file, Hardhat can also read configuration variables from an encrypted keystore (opens in a new tab).

Step 14: Compile our contract

To make sure everything is working so far, let’s compile our contract. The compile task is one of the built-in hardhat tasks.

From the command line run:

npx hardhat compile

The first time you compile, Hardhat downloads the Solidity compiler. You should then see something like Compiled 1 Solidity file with solc 0.8.34. If not, you can always message in the Alchemy Discord (opens in a new tab).

Step 15: Write our deploy script

Now that our contract is written and our configuration file is good to go, it’s time to write our contract deploy script.

Navigate to the scripts/ folder and create a new file called deploy.js, adding the following contents to it:

Let’s go through what each of these lines of code does.

const { ethers } = await network.create()

In Hardhat 3, a script opens a connection to a network explicitly. network.create() connects to the network you choose with the --network option when you run the script, and the hardhat-ethers plugin adds an ethers object to that connection.

const [deployer] = await ethers.getSigners()

getSigners() returns the accounts configured for the network. On Sepolia, that is the account whose private key you put in .env. We pass its address to the constructor so that you become the owner of the contract.

const MyNFT = await ethers.getContractFactory("MyNFT")

A ContractFactory in ethers.js is an abstraction used to deploy new smart contracts, so MyNFT here is a factory for instances of our NFT contract. When using the hardhat-ethers plugin ContractFactory and Contract instances are connected to the first signer by default.

const myNFT = await MyNFT.deploy(deployer.address)

Calling deploy() on a ContractFactory will start the deployment, and return a Promise that resolves to a Contract. This is the object that has a method for each of our smart contract functions. The arguments you pass to deploy() go to the contract's constructor, here the initialOwner address.

waitForDeployment() waits until the deployment transaction is included in a block, and getAddress() returns the address of the new contract. See the ethers.js contract documentation (opens in a new tab) for more details.

Step 16: Deploy our contract

We’re finally ready to deploy our smart contract! Navigate back to the root of your project directory, and in the command line run:

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

You should then see something like:

Contract deployed to address: 0x4C5266cCc4b3F426965d2f51b6D910325a0E7650

If we go to the Sepolia etherscan (opens in a new tab) and search for our contract address we should be able to see that it has been deployed successfully. If you can't see it immediately, please wait a while as it can take some time. The transaction will look something like this:

View your transaction address on Etherscan

The From address should match your MetaMask account address and the To address will say “Contract Creation”. If we click into the transaction, we’ll see our contract address in the To field:

View your contract address on Etherscan

Yasssss! You just deployed your NFT smart contract to the Ethereum (testnet) chain!

To understand what’s going on under the hood, you can look at the requests your app received in your Alchemy dashboard (opens in a new tab).

There you’ll see a handful of JSON-RPC calls that Hardhat/Ethers made under the hood for us when we called the .deploy() function. Two important ones to call out here are eth_sendRawTransaction, which is the request to actually write our smart contract onto the Sepolia chain, and eth_getTransactionByHash which is a request to read information about our transaction given the hash (a typical pattern when sending transactions). To learn more about sending transactions, check out the transactions documentation.

That’s all for Part 1 of this tutorial. In Part 2, we’ll actually interact with our smart contract by minting an NFT, and in Part 3 we’ll show you how to view your NFT in your Ethereum wallet!