How to Create an NFT Marketplace with Next.js and thirdweb on Polygon

How to Create an NFT Marketplace with Next.js and thirdweb on Polygon

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 are all 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.

At Lazer, we’ve worked with 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 Amoy 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 Amoy Testnet, with more than 1 MATIC in the wallet. If not, go to Polygon's Amoy Faucet to claim MATIC: https://faucet.polygon.technology/
  • Basic knowledge of Nextjs and Typescript
  • Basic knowledge of NFTs and Smart Contracts

Step 1: Create & mint your 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 Amoy (MATIC) Network
  3. Go to the NFT Collection contract you just deployed, and mint a few different NFTs. For example:
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 a new NFT Marketplace contract. Deploy new contract → NFT Marketplace → Deploy now. Keep all the configurations the same, select Amoy (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:

If you're stuck, switch to the main branch to see the finished product.

git checkout blank
npm install
npm run dev

Setup thirdweb SDKs into our application

Inside _app.tsx file, wrap everything with ThirdwebProvider Like below, with the activeChain being Amoy.

💡
An API key is required to use thirdweb's infrastructure services, such as storage, RPCs, and Smart Wallet infrastructure from within the SDK. If you haven't created a key yet, you can do so for free from the thirdweb dashboard.To use an API key with the React SDK, pass the clientId to the ThirdwebProvider.
const MyApp: AppType = ({ Component, pageProps }) => {
 return (
  <ThirdwebProvider 
  	clientId={process.env.CLIENT_ID}
    	activeChain={Amoy}>
   <Component {...pageProps} />
  </ThirdwebProvider>
 );
};

Set up 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.Amoy}>
   <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.tsxwith 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 a .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: "amoy", 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>

...

And that's it! You've got your NFT marketplace, built with Next.js and thirdweb on the Polygon network.


This guide is written by Zain Manji & Nam Dao, and was first published on Lazer's blog. Lazer is a digital product studio — designing, building, and shipping incredible products for some of the world's best brands.

Visit Lazer's website
here & get in touch with them on Twitter or email them founders@lazertechnologies.com.