A Beginner's Guide to Creating Your First NFT Minting dApp

Cover Image for A Beginner's Guide to Creating Your First NFT Minting dApp
Zach Rosen, Co-Founder
Zach Rosen, Co-Founder

Hey there!

Want to launch your first NFT mintable NFT collection? Sweet, this guide is for you!

Our goal today is to get you up-and-running with a production-ready NFT minting dApp within an hour.

Let’s get started by cloning our starter code. Open a fresh folder and run the following in your terminal:

git clone https://github.com/brydge-network/full-stack-nft-mint-tutorial.git
cd full-stack-nft-mint-tutorial

Styling isn’t the focus of our tutorial—we’re more interested in building smart contracts and triggering them. The starter code kicks us off with some cool CSS stuff we’ll use later to make our dApp’s UI look pretty.

Now, time for some contracts!

From here on out, we’ll assume that you’re in the full-stack-nft-mint-tutorial folder.

Run the following in terminal:

brownie init

This scaffolds out our back end structure for us.

Now, create a file called brownie-config.yaml in your root and paste the following:

dotenv: .env
    - smartcontractkit/chainlink-brownie-contracts@0.4.0
    - OpenZeppelin/openzeppelin-contracts@4.5.0
           - '@chainlink=smartcontractkit/chainlink-brownie-contracts@0.4.0'
           - '@openzeppelin=OpenZeppelin/openzeppelin-contracts@4.5.0'
    from_key: ${PRIVATE_KEY}

Important parts here are noting our .env file (where we keep sensitive information) and from_key (the wallet we’ll use to deploy our NFTs).

Next, create an .env file in root and paste the following. You can export your private key from MetaMask (use a separate dev wallet!) and get an Infura key for free here. We can leave the rest blank for now


It’s (finally!) smart contract time. Create a file called BrydgeCollection.sol in /contracts and paste in the following code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import '@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol';
import '@openzeppelin/contracts/token/ERC721/ERC721.sol';

contract BrydgeCollection is ERC721URIStorage {
  uint256 public tokenCounter;
  //set price to 0.001 native
  uint256 price = 1000000000000000;

  constructor() public ERC721('Brydge Tutorial NFTs', 'BRYDGE') {
    tokenCounter = 0;

  function mintBrydgeNFT(string memory tokenURI) public payable returns (bytes32) {
    require(msg.value >= price, "Send more tokens next time!");
    require(tokenCounter < 100, 'Max number of tokens reached');
    uint256 tokenId = tokenCounter;
    _safeMint(msg.sender, tokenId);
    _setTokenURI(tokenId, tokenURI);

Our NFT contract is super simple. We define our collection name (BrydgeCollection) and set a price to mint. We then set tokenCounter to zero at inception, since we haven’t minted any NFTs yet.

We then expose 1 function to the world: mintBrydgeNFT(). This function takes in a tokenURI (we’ll get to this later) and tokens. If too few tokens are paid, or this contract has already minted 100 NFTs, the transaction fails. Otherwise, it mints a fresh NFT and sends it to the buyer.

Next, we need to compile our contract into machine-readable language. Run the following in your root terminal:

brownie compile

Let’s now deploy our NFTs. Create a file called deploy_nft.py in /scripts and paste in the following:

from brownie import BrydgeCollection, accounts, config

def main():
    dev = accounts.add(config['wallets']['from_key'])
        {'from': dev}

This is as simple as can be. We set the “from” wallet to ours. We then deploy BrydgeCollection.

Next, run the following in your root terminal to deploy! We’re deploying on Rinkeby testnet, so head over to the free Rinkeby faucet to get yourself some test ETH to pay deployment gas costs.

brownie run scripts/deploy_nft.py --network rinkeby

If all goes well, we’ll get the following in our terminal.

Screen Shot 2022-05-31 at 10.34.29 PM

Copy your deployment address and paste it into search bar in https://rinkeby.etherscan.io/. Boom! We have liftoff!

Now, create an “images” folder in root and add a few images that you want to immortalize. Call them 1.jpg, 2.jpg, 3.jpg for now to keep things simple. Your file structure will look like this


Now, let’s fill out more of our .env. Grab a free API key and secret from Pinata, paste into the proper spots.

Next, create a /metadata folder in your root. This will store our precious NFT data temporarily.

Now, create a file called create_collection_data.py in /scripts and paste the following code:

import requests
import os
import json
from brownie import BrydgeCollection

metadata_template = {
    "name": "",
    "description": "",
    "image": ""

def main():
    # gets our most recent NFT deployment
    brydge_collection = BrydgeCollection[-1]
    existing_tokens = brydge_collection.tokenCounter()
    #however many nfts we want to deploy
    meta_data_hashes = write_metadata(3)

def write_metadata(num_tokens):
    # We'll use this array to store the hashes of the metadata
    meta_data_hashes = []
    for token_id in range(num_tokens):
        collectible_metadata = metadata_template.copy()
        # The filename where we're going to locally store the metadata
        meta_data_filename = f"metadata/{token_id + 1}.json"
        # Name of the collectible set to its token id
        collectible_metadata["name"] = str(token_id)
        # Description of the NFT
        collectible_metadata["description"] = f"Brydge NFT #{token_id}"
        # Path of the artwork to be uploaded to IPFS
        img_path = f"images/{token_id + 1}.jpg"
        with open(img_path, "rb") as f:
            img_binary = f.read()
        # Upload the image to IPFS and get the storage address
        image = upload_to_ipfs(img_binary)
        # Add the image URI to the metadata
        image_path = f"https://ipfs.io/ipfs/{image}"
        collectible_metadata["image"] = image_path
        with open(meta_data_filename, "w") as f:
            # Write the metadata locally
            json.dump(collectible_metadata, f)
        # Upload our metadata to IPFS
        print('collectible', collectible_metadata)

        meta_data_hash = upload_to_ipfs(collectible_metadata['image'])
        print('metadata hash', meta_data_hash)
        meta_data_path = f"https://ipfs.io/ipfs/{meta_data_hash}"
        # Add the metadata URI to the array
    with open('metadata/data.json', 'w') as f:
        # Finally, we'll write the array of metadata URIs to a file
        json.dump(meta_data_hashes, f)
    return meta_data_hashes

def upload_to_ipfs(data):
    # Get our Pinata credentials from our .env file
    pinata_api_key = os.environ["PINATA_API_KEY"]
    pinata_api_secret = os.environ["PINATA_API_SECRET"]
    endpoint = "https://api.pinata.cloud/pinning/pinFileToIPFS"
    headers = {
        'pinata_api_key': pinata_api_key,
        'pinata_secret_api_key': pinata_api_secret
    body = {
        'file': data
    # Make the pin request to Pinata
    response = requests.post(endpoint, headers=headers, files=body)
    # Return the IPFS hash where the data is stored
    return response.json()["IpfsHash"]

We first set a template object called metadata_template to standardize how we want to store each NFT’s metadata.

Our main() function kicks off our script by calling write_metadata() with the number of NFTs we want to deploy passed in. This number corresponds to the number of photos you have in /images.

write_metadata() then fills in the metadata_template for each NFT we’re deploying and calls upload_to_ipfs() to write that object to IPFS (our online file storage system that’s a decentralized version of AWS).

Now, let’s run the following command in our root terminal to pin our NFT images to IPFS:

brownie run scripts/create_collection_data.py --network rinkeby

This populates /metadata with our image data. Take a quick look to view your work!

We’re now done with the back end, let’s take a look at front end in /client. Shoutout to Abhinav for creating most of the front end scaffolding we’ll use.

Run the following in your root terminal. This enters our separate front end folder (called client) that came with our starter code, installs our dependencies, and spins up a local developer environment.

cd client
npm install
npm run dev

Remember that NFT deployment address we saved? Drop that into /client/config.js

export const nftContractAddress = 'YourDeployedNFTContractAddress'

Replace contents of /client/pages/index.js with the following:

import { useState } from 'react'
import { nftContractAddress } from '../config.js'
import { ethers } from 'ethers'
import axios from 'axios'

import NFT from '../../build/contracts/BrydgeCollection.json'
import uriList from '../../metadata/data.json'

const MintPage = () => {
	// Hooks that render data once variables are set
	const [mintedNFT, setMintedNFT] = useState(null)
	const [miningStatus, setMiningStatus] = useState(null)
	const [currentAccount, setCurrentAccount] = useState('')

	// Calls Metamask to connect wallet on clicking Connect Wallet button
	const connectWallet = async () => {
		try {
			const { ethereum } = window

			if (!ethereum) {
				console.log('Metamask not detected')
			let chainId = await ethereum.request({ method: 'eth_chainId' })
			console.log('Connected to chain:' + chainId)

			const rinkebyChainId = '0x4'

			const devChainId = 1337
			const localhostChainId = `0x${Number(devChainId).toString(16)}`

			if (chainId !== rinkebyChainId && chainId !== localhostChainId) {
				alert('You are not connected to the Rinkeby Testnet!')

			const accounts = await ethereum.request({ method: 'eth_requestAccounts' })

			console.log('Found account', accounts[0])
		} catch (error) {
			console.log('Error connecting to metamask', error)

	// Creates transaction to mint NFT on clicking Mint button
	const mintNFT = async () => {
		try {
			const { ethereum } = window

			if (ethereum) {
				const provider = new ethers.providers.Web3Provider(ethereum)
				const signer = provider.getSigner()
				const nftContract = new ethers.Contract(
				// let nftTx = await nftContract.createEternalNFT()
				let nftId = await nftContract.tokenCounter()
				nftId = await nftId.toNumber()
				console.log('about to mint Brydge NFT #', nftId)
				const nftUri = uriList[nftId]
				// const nftUri = uriList[0]
				let nftTx = await nftContract.mintBrydgeNFT(nftUri, { value: ethers.utils.parseEther("0.001") })
				console.log('Mining....', nftTx.hash)

				let tx = await nftTx.wait()
				console.log('Mined!', tx)

					`Mined, see transaction: https://rinkeby.etherscan.io/tx/${nftTx.hash}`

			} else {
				console.log("Ethereum object doesn't exist!")
		} catch (error) {
			console.log('Error minting NFT', error)

	// Gets the minted NFT data
	const getMintedNFT = async (nftId) => {
		try {
			const { ethereum } = window

			if (ethereum) {
				const provider = new ethers.providers.Web3Provider(ethereum)
				const signer = provider.getSigner()
				const nftContract = new ethers.Contract(
			//token uri is a storing of our nft's data in machine-readable format
				let tokenUri = await nftContract.tokenURI(nftId)
				let data = await axios.get(tokenUri)
				let image = data.data

			} else {
				console.log("Ethereum object doesn't exist!")
		} catch (error) {

	return (
		<div className='flex flex-col items-center pt-32 bg-[#0B132B] text-[#d3d3d3] min-h-screen'>
			<div className='trasition hover:rotate-180 hover:scale-105 transition duration-500 ease-in-out'>
					viewBox='0 0 16 16'
					<path d='M8.186 1.113a.5.5 0 0 0-.372 0L1.846 3.5 8 5.961 14.154 3.5 8.186 1.113zM15 4.239l-6.5 2.6v7.922l6.5-2.6V4.24zM7.5 14.762V6.838L1 4.239v7.923l6.5 2.6zM7.443.184a1.5 1.5 0 0 1 1.114 0l7.129 2.852A.5.5 0 0 1 16 3.5v8.662a1 1 0 0 1-.629.928l-7.185 2.874a.5.5 0 0 1-.372 0L.63 13.09a1 1 0 0 1-.63-.928V3.5a.5.5 0 0 1 .314-.464L7.443.184z' />
			<h2 className='text-3xl font-bold mb-20 mt-12'>
				Mint Brydge NFT
			{currentAccount === '' ? (
					className='text-2xl font-bold py-3 px-12 bg-black shadow-lg shadow-[#6FFFE9] rounded-lg mb-10 hover:scale-105 transition duration-500 ease-in-out'
					Connect Wallet
			) : (
					className='text-2xl font-bold py-3 px-12 bg-black shadow-lg shadow-[#6FFFE9] rounded-lg mb-10 hover:scale-105 transition duration-500 ease-in-out'
					Mint Brydge NFT for 0.001 ETH
			{miningStatus === 1 ? (
				className='h-60 w-60 rounded-lg shadow-2xl shadow-[#6FFFE9] hover:scale-105 transition duration-500 ease-in-out'
			) : (

export default MintPage

Here, we define our React component called “MintPage” and add a few functions triggered on button click.

We then add some HTML in the return() statement. We’ll see that we check if the user is signed in via Metamask. If not, we’ll show a button for the user to connect their wallet. If so, we’ll allow the user to mint our NFT.

We then have a final check to see if our transaction has been submitted and confirmed on the blockchain (mined). If this is true, we’ll show an image of the NFT we just minted. If not, we’ll show nothing.

Save the file, then head to localhost:3000 in your browser and you’ll see our minting UI.

Click “Connect Wallet” button to connect your MetaMask. Switch to Rinkeby, since that’s where we deployed!

Right click in your browser, click “Inspect”, then click the “Console” tab so we can see some information being logged.

If you’re using the same wallet to mint that you deployed with, you can now click the “Mint” button and watch the magic happen. If not, head back to to https://rinkebyfaucet.com/ to get some more Rinkeby ETH.

Once your transaction is confirmed on the Blockchain, you’ll see your NFT-ified image pop up. Mint again and you’ll see the next NFT in your collection. Check your wallet transaction history on the Rinkeby block explorer and you’ll see each action confirmed on the blockchain.

Want to deploy on a live network so real people can buy + mint your work? Add some real MATIC to your wallet via the free Polygon faucet, run the following in your terminal in /client.

cd ..
brownie run scripts/deploy_nft.py --network polygon-mainnet
brownie run scripts/create_collection_data.py --network polygon-mainnet

And that’s a wrap! You’ve deployed a full-stack NFT minting application on both testnet and mainnet.

This tutorial gets you up-and-running with a collection mintable using the native token on one chain (ETH on Ethereum / Rinkeby and MATIC on Polygon). Each chain brings about pros and cons:

  • Ethereum
    • Pros: largest potential customer base + easiest onboarding
    • Cons: super expensive to deploy ($200+) and mint ($50+) NFTs
  • Polygon
    • Pros: <$0.01 transaction fees
    • Cons: users have to bridge tokens from Ethereum —> Polygon before using your dApp

Brydge provides the best of both worlds, enabling you to deploy on one chain and instantly accept any token from the rest!

So, you can deploy on Polygon to take advantage of sub-1 cent deployment costs and user transaction fees, while seamlessly accepting payments from users on Ethereum. Plus, you can instantly accept USDC, LINK, DAI, and thousands of other tokens without redeploying your contracts!

Learn more at www.brydge.network and view SDK documentation here.

We’re always here to help in our Discord!

Top Posts

Recent Posts

More Stories

Cover Image for Liquidity Pool Deposit Launch

Liquidity Pool Deposit Launch

Brydge Liquidity enables your users to add liquidity to your pools in 1-click with any token from any supported chain

Zach Rosen, Co-Founder
Zach Rosen, Co-Founder
Cover Image for Unifying All Balances Across All Supported Chains

Unifying All Balances Across All Supported Chains

Until today, you’ve needed to choose a source chain from before viewing your token balances. We’ve received tons of feedback that you want this to be simpler. Welcome, unified token list.

Zach Rosen, Co-Founder
Zach Rosen, Co-Founder

© 2022 Brydge Inc. All rights reserved


OverviewDocsBook a DemoIntegratePlaygroundAuditReport a Bug