Blog

How To Create An NFT Marketplace With Next.js And Thirdweb On Polygon

November 01, 2022Nam Dao

You’ve probably heard of NFTs before but just-in-case, non-fungible tokens (NFTs) are unique digital identifiers that cannot be copied, substituted, or subdivided. They are recorded on a blockchain to certify authenticity and ownership.

As the popularity of NFTs has grown, the NFT market has climbed to more than $40 billion in value. And currently, the best way to buy and sell NFTs is through NFT marketplaces.

NFT marketplaces like OpenSea, Axie Marketplace, NBA Top Shot, Rarible, Nifty Gateway, Sudoswap, and more have all be leaders in this space. They act as your gateway to participating in the purchase and sale of digital assets, from art to music to entire virtual worlds.

Through Lazer, we’ve worked on many NFT marketplace projects including Sudoswap, Stardust, Genies, NFL All Day, and we thought it would be great to walkthrough a tutorial on how to build a simplified version of an NFT marketplace

In this tutorial, we will build an NFT marketplace on the Polygon Mumbai testnet using the Next.js framework with Typescript and the Thirdweb SDK.

The end result will look similar to the images below.

Basic NFT Marketplace Listings
Basic NFT Profile Page
Basic NFT Marketplace Collection Page

After building the NFT marketplace, it should be able to:

  • See all the available NFTs for sale

  • View individual NFTs and their details

  • Allow users to buy the NFTs

  • (Bonus) View all the NFTs in a wallet

Prerequisites we need in order to accomplish this:

  • A digital wallet (e.g. Metamask)

  • The wallet is connected to the Polygon Mumbai Testnet, with more than 1 MATIC in the wallet. If not, go to Alchemy’s Mumbai Faucet to claim 1 MATIC https://mumbaifaucet.com/

  • Basic knowledge of Nextjs and Typescript

  • Basic knowledge of NFTs and Smart Contracts

Step 1: Create/mint some NFTs on Thirdweb

Before we can create the NFT marketplace, we first need to create and NFT Collection contract and mint some NFTs. The NFT Collection contract is suitable for when you want to have a collection of unique NFTs, but not "drop" or "release" them for your community to claim.

  1. Go to your Thirdweb dashboard

  2. Create a new NFT Collection contract. Click Deploy new contract → NFT Collection → Deploy now. Keep all the configurations the same, select Mumbai (MATIC) Network

  3. Go to the NFT Collection contract you just deployed, and mint a few different NFTs

Create & Mint NFTs On Thirdweb

Step 2: Create and deploy an NFT Marketplace contract on Thirdweb

Now that you've minted a few NFTs, let's create and deploy an NFT Marketplace contract. An NFT Marketplace contract is a contract where you can buy and sell NFTs, such as OpenSea or Rarible. The Marketplace contract allows users to list NFTs for direct sale or auction. Other users can place offers/bids or buy the NFTs for the specified amount in the listing. The marketplace can be configured to only allow certain users to list NFTs for sale, or allow any user to list NFTs for sale.

  1. Go to the Thirdweb dashboard

  2. Create new NFT Marketplace contract. Deploy new contract → NFT Marketplace → Deploy now. Keep all the configurations the same, select Mumbai (MATIC) Network.

  3. Go to the marketplace contract you just deployed, and create listings. For this tutorial, please select the Direct listing type.

Create and deploy an NFT Marketplace contract on Thirdweb

Step 3: Let’s write some Typescript code!

First, let’s clone a GitHub repo that already has styling done for you

Disclaimer: This is not a CSS/HTML tutorial, so let's get started by cloning this repository from GitHub that already has styling: https://github.com/LazerTechnologies/nft-marketplace-tutorial

Once cloned, run:

  1. git checkout blank (If you are ever stuck, switch to the main branch for the finished product)

  2. npm install

  3. npm run dev

Setup Thirdweb SDKs into our application

Inside "_app.tsx" file, wrap everything with ThirdwebProvider Like below, with the chain id being Mumbai.

const MyApp: AppType = ({ Component, pageProps }) => { return ( <ThirdwebProvider desiredChainId={ChainId.Mumbai}> <Component {...pageProps} /> </ThirdwebProvider> ); };

Setup Web3 Authentication using Thirdweb

Create a file called "components/AuthProvider.tsx"

import { ReactElement, useEffect, useState } from "react"; import { ConnectWallet, useAddress } from "@thirdweb-dev/react"; export default function AuthProvider({ children, }: { children: React.ReactNode; }): ReactElement { const [isLoggedin, setIsLoggedin] = useState(false); const address = useAddress(); useEffect(() => { if (address) { setIsLoggedin(true); } else { setIsLoggedin(false); } }, [address]); if (!isLoggedin) { return ( <div className={"flex h-screen w-full items-center justify-center"}> <div> <h1 className={"text-lg"}>Please login to continue...</h1> <ConnectWallet /> </div> </div> ); } return <>{children}</>; }

Wrap the <Component> in "_app.tsx" with AuthProvider

const MyApp: AppType = ({ Component, pageProps }) => { return ( <ThirdwebProvider desiredChainId={ChainId.Mumbai}> <AuthProvider> <Component {...pageProps} /> </AuthProvider> </ThirdwebProvider> ); };

Now the user will see this when they’re not logged in.

Simple Connect Wallet Page

Fetch the NFTs listed on your Marketplace contract

In "index.tsx", fetch the marketplace contract using useContract, and fetch the active listings using "useActiveListings"

import type { NextPage } from "next"; import { useActiveListings, useContract } from "@thirdweb-dev/react"; const Home: NextPage = () => { const { contract } = useContract( "<YOUR MARKETPLACE CONTRACT ADDRESS>", "marketplace" ); const { data, isLoading } = useActiveListings(contract); console.log(data); return ( <div>NFT Marketplace</div> ); }; export default Home;

Open the web console and you should see the NFT data 😊

Developer Console NFT Data

Now let’s create a component to show each of these NFTs.

Let’s make "components/NFTCard.tsx" file and add the following:

import Image from "next/image"; import { MediaRenderer } from "@thirdweb-dev/react"; export default function NFTCard({ nft, }: { nft: { tokenUri: string; name: string; price?: string; }; }) { return ( <div className={`relative flex cursor-pointer flex-col overflow-hidden rounded-lg bg-white shadow-lg transition-all duration-300 hover:shadow-2xl dark:bg-[#333333]`} > <MediaRenderer src={nft.tokenUri} style={{ objectFit: "cover", }} className={ "h-[244px] rounded-lg transition duration-300 ease-in-out hover:scale-105" } /> <div className={`flex flex-col gap-y-3 p-3`}> <div className={`text-sm font-semibold`}>{nft.name}</div> {nft.price && ( <div> <div className={`text-xs font-semibold`}>Price</div> <div className={`flex items-center gap-x-1`}> <Image src={"/matic-logo.png"} height={16} width={16} /> <p className={`text-base font-semibold`}>{nft.price}</p> </div> </div> )} </div> </div> ); }

Let's display this data properly, adding the following to the "styles/global.css"

@tailwind base; @tailwind components; @tailwind utilities; html { font-family: "DM Sans", sans-serif; background: white; } .nft-grid { display: grid; --template-column-gutters: 8px; --template-columns: 1; --template-column-compact-multiplier: 1; --template-reduced-columns: 0; --template-reduced-columns-multiplier: 1; gap: var(--template-column-gutters); grid-auto-rows: minmax(0px, 1fr); grid-template-columns: repeat( calc(var(--template-columns) - (var(--template-reduced-columns) * var(--template-reduced-columns-multiplier))), minmax(0, 1fr) ); } @media (min-width: 20rem) { .nft-grid { --template-columns: 2; } } @media (min-width: 30rem) { .nft-grid { --template-columns: 3; } } @media (min-width: 40rem) { .nft-grid { --template-columns: 4; } } @media (min-width: 768px) { .nft-grid { --template-column-gutters: 16px; } }

Now replace the "index.tsx" with the code below:

import type { NextPage } from "next"; import { useActiveListings, useContract } from "@thirdweb-dev/react"; import NFTCard from "../components/NFTCard"; const Home: NextPage = () => { const { contract } = useContract( "YOUR MARKETPLACE CONTRACT", "marketplace" ); const { data: nfts, isLoading } = useActiveListings(contract); if (isLoading) return ( <div className={"mb-3 flex w-screen justify-center"}>Loading ...</div> ); return ( <div className={"space-y-4 p-2"}> <div className={"text-2xl font-semibold"}>Active Listings</div> <div className={`nft-grid`}> {nfts && nfts.map((nft) => { return ( <a> <NFTCard nft={{ name: nft.asset.name as string, tokenUri: nft.asset.image as string, price: nft.buyoutCurrencyValuePerToken?.displayValue, }} /> </a> ); })} </div> </div> ); }; export default Home;

Now your home page should look like the below!

Basic NFT Marketplace Listings

Now that we have the home page, let's display individual NFT listings.

For this, we will create a file called: "pages/assets/[listingid].tsx"

import { useRouter } from "next/router"; import { useContract, useListing } from "@thirdweb-dev/react"; import Image from "next/image"; import { AiOutlineClockCircle } from "react-icons/ai"; import { BigNumber } from "ethers"; export default function NFT() { const router = useRouter(); const { listingId } = router.query; const { contract } = useContract( "YOUR MARKETPLACE CONTRACT", "marketplace" ); const { data: nft, isLoading } = useListing(contract, listingId as string); const buyoutListing = async () => { try { await contract?.buyoutListing(BigNumber.from(listingId), 1); } catch (e) { alert(e); } }; if (isLoading || !nft) return ( <div className={"flex h-screen items-center justify-center"}> Loading ... </div> ); return ( <div className="flex justify-center"> <div className="flex max-w-[500px] flex-col justify-center gap-y-4 p-2"> <div className={"text-2xl font-semibold"}>{nft?.asset?.name}</div> <div className={"flex flex-col rounded-lg border border-[#e8ebe5]"}> <div className={`flex items-center justify-start p-3`}> <Image src={`/matic-logo.png`} height={20} width={20} /> </div> <Image className={"rounded-2xl"} src={nft?.asset.image as string} width={500} height={500} objectFit={"cover"} /> </div> <div className={"flex space-x-1 text-sm"}> <div className={"text-gray-500"}>Owned by</div> <div className="cursor-pointer text-blue-500"> {nft?.sellerAddress} </div> </div> {/*Bottom Section*/} <div className={"flex flex-col rounded-lg border border-[#e8ebe5]"}> <div className={"border-b border-[#e8ebe5] p-3"}> <div className={ "flex items-center space-x-2 text-sm text-gray-700 md:text-base" } > <AiOutlineClockCircle size={24} /> <p>Sale ends November 26, 2022 at 7:39pm GMT+11</p> </div> </div> <div className={"flex flex-col gap-y-2 bg-slate-50 p-3"}> <div className={"text-sm text-gray-500"}>Current Price</div> <div className={`flex items-center space-x-3`}> <Image src={`/matic-logo.png`} height={24} width={24} /> <p className={`text-3xl font-semibold`}> {nft?.buyoutCurrencyValuePerToken?.displayValue} </p> </div> <button type="button" className="rounded-lg bg-blue-700 px-5 py-4 text-base font-bold text-white hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800" onClick={buyoutListing} > Purchase </button> </div> </div> </div> </div> ); }

Now add the link to this page from the "index.tsx"

const Home: NextPage = () => { ... return ( ... <Link href={`/assets/${nft.id}`} key={nft.assetContractAddress + nft.id} > <a> <NFTCard nft={{ name: nft.asset.name as string, tokenUri: nft.asset.image as string, price: nft.buyoutCurrencyValuePerToken?.displayValue, }} /> </a> </Link> ... ); };

Now this page should look like the below:

Basic NFT Profile Page

Now when you hit purchase, it should pop up with the Metamask wallet.

NFT Profile Page With Metamask Open

(Bonus) View all the NFTs in your wallet

Now that we have the home page and an NFT page, let’s create a page that shows all the NFTs existing in our wallet. For this, the easiest approach is to use Moralis’s Web3 API. It's free! Please signup to their website and create your API key.

Moralis Web3 API

Once you’ve got your API keys, create an .env in the root folder and add:

MORALIS_API_KEY=<YOUR API KEY>

Let’s utilize Next.js’s backend feature and create "pages/api/wallet/[walletAddress]/nfts.ts" with the following code:

import { NextApiRequest, NextApiResponse } from "next"; import axios from "axios"; export default async function handle( req: NextApiRequest, res: NextApiResponse ) { const { walletAddress } = req.query; const options = { method: "GET", url: `https://deep-index.moralis.io/api/v2/${walletAddress}/nft`, params: { chain: "mumbai", format: "decimal" }, headers: { accept: "application/json", "X-API-Key": process.env.MORALIS_API_KEY, }, }; try { const { data } = await axios.request(options); const results: { tokenUri: any; name: any }[] = data.result.map( (nft: any) => ({ name: JSON.parse(nft.metadata)?.name, tokenUri: JSON.parse(nft.metadata)?.image, }) ); const filteredResults = results.filter( (data) => !!data?.tokenUri && !!data?.name ); res.status(200).json(filteredResults); } catch (error) { console.error(error); res.status(500).json({ error: error }); } }

Here, we’ve fetched the NFTs given a wallet address. Then, we restructured/filtered the data and returned it to the client.

Now that we have our NFTs API, let's create "pages/profile/[walletAddress].tsx" in the frontend with the following code:

import { useRouter } from "next/router"; import NFTCard from "../../components/NFTCard"; import Image from "next/image"; import { fetcher } from "../../utils/utils"; import useSWR from "swr"; export default function Profile() { const router = useRouter(); const { walletAddress } = router.query; const { data } = useSWR(`/api/wallet/${walletAddress}/nfts`, fetcher); if (!data) return ( <div className={"flex h-screen w-screen items-center justify-center"}> Loading ... </div> ); return ( <div className={"flex w-screen flex-col justify-center gap-y-2 p-4"}> <div className={"space-y-2"}> <div className={"text-3xl font-semibold"}>Your collection</div> <div> <div className={"flex items-center space-x-1"}> <Image src={`/matic-logo.png`} height={16} width={16} /> <p className={"w-1/4 truncate text-slate-400"}>{walletAddress}</p> </div> </div> </div> {data?.length === 0 ? ( <div>No NFTs were found...</div> ) : ( <div className={`nft-grid`}> {data?.map((nft: any, index: number) => { return ( <NFTCard key={index} nft={{ name: nft.name, tokenUri: nft.tokenUri, }} /> ); })} </div> )} </div> ); }

Now let's add a link to this "profile/[walletAddress]" page in our "index.tsx"

import type { NextPage } from "next"; import { useActiveListings, useAddress, useContract, } from "@thirdweb-dev/react"; import Link from "next/link"; import NFTCard from "../components/NFTCard"; const Home: NextPage = () => { const address = useAddress(); ... return ( <div className={"space-y-4 p-2"}> <div className={"flex space-x-4"}> <div className={"text-2xl font-semibold"}>Active Listings</div> <Link href={`profile/${address}`}> <div className={"cursor-pointer text-2xl font-semibold"}> My Collections </div> </Link> </div> ...

That’s it! 😊

Thanks for going through the tutorial. If you have any questions throughout, feel free to send us an email at founders@lazertechnologies.com.

At Lazer, we've helped some of the best brands navigate this space, such as Sudoswap, Genies, NFL All Day, Stardust, and more, so if you're interested in us helping you with this as well, send us an email!

Please share with anyone you feel may find value from it as well 🤗