The Bitaigen editorial team believes that this article systematically outlines the complete steps for writing, deploying, and minting NFTs within the Flow and IPFS environments. From tool installation to Cadence contract implementation, each step is explained in detail to help developers get started quickly. The upcoming hands‑on examples are especially promising, so a careful read is recommended.
Tutorial for Creating NFT Contracts and Minting Tokens with Flow and IPFS
When Flow is used together with IPFS, you can write a Cadence contract, deploy it to the emulator or a testnet, upload assets to Pinata to obtain a CID, and then mint an NFT in a transaction while binding its metadata to the token. This yields a full end‑to‑end creation and minting workflow.
Environment Setup
- Install the Flow CLI
- macOS
```bash
brew install flow-cli
```
- Linux
```bash
sh -ci "$(curl -fsSL https://storage.googleapis.com/flow-cli/install.sh)"
```
- Windows
```powershell
iex "& { $(irm 'https://storage.googleapis.com/flow-cli/install.ps1') }"
```
- Prepare IPFS Asset Storage
- Register a Pinata free account and obtain an API Key. The second part of this series will use the Pinata API; this article only covers manual uploads through the Pinata website.
- Install Node.js and a Code Editor
- We recommend Visual Studio Code together with the Cadence syntax extension, which provides proper highlighting for smart‑contract code.
- Create a Project Directory
```bash
mkdir pinata-party
cd pinata-party
flow project init
```
After the initialization, a `flow.json` file is generated; it will be edited later.
- Organize the Code Structure
```
pinata-party/
├─ cadence/
│ └─ contracts/
│ └─ PinataPartyContract.cdc
├─ transactions/
└─ scripts/
```
All Flow‑related source files belong in `cadence/contracts`. Transaction scripts and read‑only query scripts will be placed in `transactions` and `scripts`, respectively.
Configuring `flow.json` for the Emulator
Add the contract path to `flow.json`:
```json
"contracts": {
"PinataPartyContract": "./cadence/contracts/PinataPartyContract.cdc"
},
"deployments": {
"emulator": {
"emulator-account": ["PinataPartyContract"]
}
}
```
This configuration tells the Flow CLI to deploy `PinataPartyContract` to the local emulator.
Writing the NFT Contract
Core Contract Structure
```cadence
pub contract PinataPartyContract {
// NFT resource definition
pub resource NFT {
pub let id: UInt64
init(initID: UInt64) {
self.id = initID
}
}
// Interface for receiving NFTs
pub resource interface NFTReceiver {
pub fun deposit(token: @NFT, metadata: {String: String})
pub fun getIDs(): [UInt64]
pub fun idExists(id: UInt64): Bool
pub fun getMetadata(id: UInt64): {String: String}
}
// Implementation of an NFT Collection
pub resource Collection: NFTReceiver {
// Storage for NFTs
pub var ownedNFT: @{UInt64: NFT}
// Mapping for corresponding metadata
pub var metadataObjs: {UInt64: {String: String}}
init() {
self.ownedNFT <- {}
self.metadataObjs = {}
}
// Withdraw an NFT
pub fun withdraw(withdrawID: UInt64): @NFT {
let token <- self.ownedNFT.remove(key: withdrawID)!
return <-token
}
// Deposit an NFT and bind metadata
pub fun deposit(token: @NFT, metadata: {String: String}) {
self.ownedNFT[token.id] <-! token
self.metadataObjs[token.id] = metadata
}
pub fun idExists(id: UInt64): Bool {
return self.ownedNFT[id] != nil
}
pub fun getIDs(): [UInt64] {
return self.ownedNFT.keys
}
pub fun getMetadata(id: UInt64): {String: String} {
return self.metadataObjs[id]!
}
destroy() {
destroy self.ownedNFT
}
}
// Factory function that creates an empty Collection
pub fun createEmptyCollection(): @Collection {
return <-create Collection()
}
// Minter resource
pub resource NFTMinter {
pub var idCount: UInt64
init() {
self.idCount = 1
}
pub fun mintNFT(): @NFT {
let newNFT <- create NFT(initID: self.idCount)
self.idCount = self.idCount + 1
return <-newNFT
}
}
// Contract initializer: deploy the Collection, expose the interface, store the minter
init() {
// Create and store an empty Collection for the contract deployer
self.account.save(<-self.createEmptyCollection(), to: /storage/NFTCollection)
// Publish the Collection as an NFTReceiver capability
self.account.link<&Collection{NFTReceiver}>(/public/NFTReceiver, target: /storage/NFTCollection)
// Store the NFTMinter resource; only the contract creator can access it
self.account.save(<-create NFTMinter(), to: /storage/NFTMinter)
}
}
```
Key Points Explained
- The NFT resource contains only a unique identifier `id`.
- The NFTReceiver interface defines four externally callable methods: `deposit`, `getIDs`, `idExists`, and `getMetadata`.
- The Collection resource implements this interface and additionally maintains `metadataObjs` to store each NFT’s metadata.
- NFTMinter handles the incremental `idCount` and creates new NFTs.
- The `init()` function automatically creates an empty Collection, publishes the capability, and stores the minter resource when the contract is deployed.
Deploying the Contract
- Deploy via the Flow Playground or the local emulator.
- Using the local emulator, run:
```bash
flow project start-emulator # Launch the local emulator
flow project deploy # Deploy the contract
```
A successful deployment prints a log similar to:
```
Deploying 1 contracts for accounts: emulator-account.PinataPartyContract -> 0xf8d6e0586b0a20c7
```
Minting an NFT (Transaction Script)
Create the Transaction File
Add a new file `MintPinataParty.cdc` inside the `transactions` folder with the following content:
```cadence
import PinataPartyContract from 0xf8d6e0586b0a20c7
transaction {
// References to the receiver and the minter
let receiverRef: &{PinataPartyContract.NFTReceiver}
let minterRef: &PinataPartyContract.NFTMinter
prepare(acct: AuthAccount) {
// Obtain the public NFTReceiver capability
self.receiverRef = acct.getCapability<&{PinataPartyContract.NFTReceiver}>(/public/NFTReceiver)
.borrow()
?? panic("Could not borrow receiver reference")
// Borrow the private NFTMinter resource
self.minterRef = acct.borrow<&PinataPartyContract.NFTMinter>(from: /storage/NFTMinter)
?? panic("Could not borrow minter reference")
}
execute {
// Example metadata (replace the IPFS CID with your own)
let metadata: {String: String} = {
"name": "The Big Swing",
"swing_velocity": "29",
"swing_angle": "45",
"rating": "5",
"uri": "ipfs://QmRZdc3mAMXpv6Akz9Ekp1y4vDSjazTx2dCQRkxVy1yUj6"
}
// Mint the NFT and deposit it into the Collection
let newNFT <- self.minterRef.mintNFT()
self.receiverRef.deposit(token: <-newNFT, metadata: metadata)
log("NFT minted and deposited")
}
}
```
Explanation
- Replace the address in the `import` line with the actual address where the contract was deployed.
- The `uri` field in `metadata` uses the `ipfs://` scheme and should point to the CID you obtained after uploading the asset to Pinata.
- The `deposit` call writes both the NFT and its metadata into the Collection.
Generate an Account Key and Update `flow.json`
```bash
flow keys generate
```
Copy the returned `privateKey` and `publicKey` into `flow.json` as shown:
```json
"accounts": {
"emulator-account": {
"address": "0xf8d6e0586b0a20c7",
"privateKey": "YOUR_PRIVATE_KEY",
"chain": "flow-emulator",
"sigAlgorithm": "ECDSA_P256",
"hashAlgorithm": "SHA3_256"
}
}
```
Security Note: Never commit private keys to a public repository. It is advisable to add `flow.json` to `.gitignore`.
Send the Transaction
```bash
flow transactions send --code ./transactions/MintPinataParty.cdc --signer emulator-account
```
If the command succeeds, a transaction ID is returned, confirming that the NFT has been written to the account’s Collection.
Querying NFT Metadata (Read‑Only Script)
Create a script called `CheckTokenMetadata.cdc` inside the `scripts` folder:
```cadence
import PinataPartyContract from 0xf8d6e0586b0a20c7
pub fun main(): {String: String} {
let nftOwner = getAccount(0xf8d6e0586b0a20c7)
let capability = nftOwner.getCapability<&{PinataPartyContract.NFTReceiver}>(/public/NFTReceiver)
let receiverRef = capability.borrow()
?? panic("Could not borrow the receiver reference")
return receiverRef.getMetadata(id: 1)
}
```
Execute the script:
```bash
flow scripts execute ./scripts/CheckTokenMetadata.cdc
```
The expected output resembles:
```
{"name":"The Big Swing","swing_velocity":"29","swing_angle":"45","rating":"5","uri":"ipfs://QmRZdc3mAMXpv6Akz9Ekp1y4vDSjazTx2dCQRkxVy1yUj6"}
```
At this point, the complete workflow—creating an NFT on Flow, storing its associated media on IPFS, minting the token, and retrieving its metadata—has been fully demonstrated.
---
Summary
- Write an NFT contract that complies with Flow standards using Cadence.
- Upload media files to IPFS through Pinata to obtain a CID.
- Insert the CID into the `uri` field of the metadata within a transaction script, thereby linking the on‑chain NFT to off‑chain assets.
- This tutorial covers environment setup, contract development, deployment, minting, and querying—four essential stages that lay the groundwork for building front‑end displays or secondary‑market integrations.

---
*This translation was sponsored by Cell Network. Original article link: https://medium.com/pinata/how-to-create-nfts-like-nba-top-shot-with-flow-and-ipfs-701296944bf*
Related Reading
- GameFi, Metaverse & NFT: Solana & Avalanche Power Crypto
- Top Solana Blockchain Explorers: Solscan, Solana Beach, Step & More
- Earn Money in Web3 2026: NFTs, Play-to-Earn & DeFi
💡 Register on Binance with referral code B2345 for the maximum trading fee discount. See Binance complete guide.