How to quickly create an IPFS powered app with Next.js

When building apps, developers generally just want to get to coding. They don’t want to set up all the scaffolding necessary to begin, but it’s part of life. This is where starter templates make a world of difference.
Today, we’re going to build a simple app that allows users to upload files from the browser to IPFS. We’ll use Pinata’s free plan to both handle the uploads but also to generate a link to the upload for sharing. To help us with the scaffolding problem, we’ll make use of the Pinata Next.js Starter Template.
Getting Started
Before we begin, let’s make sure we’re ready to write some code. You’ll need the following:
- Node.js version 16 or above
- NPM version 8 or above
- A good code editor
- A Pinata account
You can check your Node version from the command line like this:
You can check your NPM version similarly:
When you’re ready to sign up for your free Pinata account, head over to Pinata’s pricing page and select the free plan. And that’s it. You’re ready to code.
Creating the Scaffolding
From the command line be sure to switch into the directory where you have all your dev projects. Now, we’re going to create a new project called <span class="code-inline">simple-ipfs</span>. From the command line, run the following command:
This will kick off the CLI tool and you’ll be prompted to answer some questions. First, give your app a name (simple-ipfs). Next, decide if you want to be working in TypeScript or JavaScript. I’ll choose JavaScript for this tutorial to keep it simple. Finally, you’ll be asked if you want to use Tailwind with your project. I’m going to choose Yes, but you don’t have to.
Within a few seconds, you should have a new project ready to code. Switch into the new project directory:
Then, open the project in your code editor of choice. We need to set up our environment variables file. You’ll notice there is an <span class="code-inline">.env.sample</span> file. We’ll just copy that file and re-name it to <span class="code-inline">.env.local</span>. In the file, you’ll see three variables. The first two are the only two required variables, but we’ll talk about the other variable shortly.
Let’s start by getting our Pinata JWT. To do this, you should log into your Pinata account. Once you’re logged in, go to the API Key link on the left sidebar. Here, you can create a new key. You’ll need to copy the JWT you receive and paste it in after the <span class="code-inline">=</span> sign in your <span class="code-inline">.env.local</span> file for the variable <span class="code-inline">PINATA_JWT</span>. You can create an admin key for this project, but if you want to learn about creating API keys with granular scopes, you can read more here.
Next, you’ll want to get the URL to your Dedicated IPFS gateway. This guide explains how to do so and what Dedicated Gateways can do. Every Pinata account comes with a Dedicated Gateway. On paid plans, your access levels and bandwidth restrictions are much higher, but for the sake of this app, the Free plan should work fine. When you have your Dedicated Gateway URL, add it to your <span class="code-inline">.env.local</span> file the same way you added your JWT.
The last variable in the <span class="code-inline">.env.local</span> file is optional unless you are looking to fetch content from the IPFS network that you have not pinned yourself. This app will require uploading and and pinning content, so we don’t need to make use of the <span class="code-inline">NEXT_PUBLIC_GATEWAY_TOKEN</span> variable.
With those variables in place, we’re ready to fire up the app. Run the following from your command line:
When you visit <span class="code-inline">localhost:3000</span> in your browser, you should see a page like this:

This is nice, but let’s build our own simple uploader and routing system to share file details.
Building The App
Our app is a simple uploader that allows you to share a link with others to download the file that is shared. With that in mind, let’s design our entry page. It should have a an upload button, and we should allow the user to give the upload a name and a description.
If you open the <span class="code-inline">pages/index.js</span> file, you’ll see there’s a lot of good stuff here already, including a file input element and a function to handle the upload. We don’t want to gut the entire thing, but we definitely need to change some things up. So, let’s replace that entire file with:
We re-used a lot of the functionality that came out of the box, but we re-styled the app to match our personal flavor, and we changed the upload functionality to take a form submission with a name and description for the file. I won’t spend time going through the UI code because it’s, well, UI. However, pay attention to the <span class="code-inline">uploadFile</span> function and the <span class="code-inline">loadRecent</span> function (which is not currently used). These functions are calling our Next.js serverless backend. The <span class="code-inline">loadRecent</span> function will be used later on a different page, but we’ll hang on to it here for a bit.
Since we are passing metadata about our file to our serverless function, we need to make a minor tweak to the existing out-of-the-box code that comes with the starter template API. Open up the <span class="code-inline">pages/api/files.js</span> file and find the line that says:
We’re going to update that to:
This is taking the fields that were passed through as part of the multipart formdata upload from the frontend and using them in the <span class="code-inline">saveFile</span> function. We’ll need to update that function as well. So find it in the code and update it to look like this:
All we’ve changed here is adding the name and a keyvalue pair for the description. This data is not stored on IPFS but is a nice convenience layer provided by Pinata. When we load the file, we can show this info in-app.
Now, you may have noticed in the <span class="code-inline">pages/index.js</span> file there was a component that is called <span class="code-inline">Files</span>. We haven’t changed that yet, but we’re going to. That component displays a content identifier (CID) for the file uploaded and a link to view the file. We want to change this to display a link that can be shared with others that is a page within our app.
Let’s open up the <span class="code-inline">components/Files.jsx</span> file. We want to show the CID for the file but also include a copy button that will share the link to the file. Currently, this component has the CID and a link to view or download the file directly from a Dedicated Gateway. Let’s make some changes. Update the component to look like this:
The first thing you might notice are the SVG elements. We’re using some SVG icons from Heroicons, a great open source icon library, to help make this component look nice. We’ve got a share icon and a copy icon with the file’s CID in between.
We’re breaking the rules of semantic HTML in order to get through this tutorial quickly by adding a click handler to our DIV element. You should use a button element or the appropriate Aria props for accessibility in production. Our DIV click handler simply copies the URL to share with others.
Notice in the <span class="code-inline">copyLink</span> function we are building the link assuming there will be another page in our app that points to the CID. Let’s build this page.
In the<span class="code-inline">pages</span> folder of your project, add a new file called <span class="code-inline">[cid].js</span>. This is a way to tell Next.js that the file will use dynamic routing. Basically, anything after the forward-slash in your domain, in this case, would use this page.
Inside your <span class="code-inline">pages/[cid].js</span> file, add the following:
There’s a lot going on in this file, but we’ll walk through it. Let’s start at the bottom since the code in the <span class="code-inline">getServerSideProps</span> function runs server-side before any of the client code is rendered. This is a Next.js function that allows you to make data requests that will hydrate the frontend with the response.
In our <span class="code-inline">getServerSideProps</span> function, we are importing the Pinata SDK and using a non-public environment variable (just like we did in our serverless function API route) to use the SDK. We are querying for the CID of the file we shared, and that CID is contained in the URL.
<span class="code-inline">myApp</span>
With our result, we pass it as props that are ultimately available in our client-side component. We use the props to render the name and description of our file from the Pinata metadata. We also have a download link that triggers the <span class="code-inline">download</span> function.
This function first makes a request to the Gateway you set in your environment variables earlier to download the file into memory. We then grab the headers to identify what type of file it is through the <span class="code-inline">content-type</span> property. With that, we use a library called <span class="code-inline">mime</span> to map that <span class="code-inline">content-type</span> to a file extension. Next, the function checks to see if the browser supports the File System API. If it does, we display a download modal with a pre-populated file name and extension. And if the browser doesn’t support the File System API, we trigger a hidden link in the component and redirect to the file’s link in the browser. It will either display the file if supported by the browser, or it will download the file to the user’s computer.
Let’s see this in action.
When a user opens the share link you send them in a browser, they will have a description shown to them and the option to download. If they download the file, they can rename it if they wish and choose where on their computer it gets stored.
And that’s it! The whole app creation was accelerated massively by starting with the Next.js template from Pinata.
Wrapping Up
Next.js is one of the most popular frameworks for building React apps. IPFS is the number one storage solution for off-chain data. Now, the two are combined in an easy to use start template. This particular example uses serverless functions to upload files. This can be tricky because of the limits platforms like Vercel and AWS have on serverless functions payload size. If you want to upload larger files, stay tuned for a future tutorial where we show you how to create a signed token to do uploads right from the client safely.
If you want to see the full code for this app, you can find it on Github here.
Until then, happy pinning!
H1 - THE RICH TEXT
EXAMPLE STARTS HERE
H2 - Enabling Widespread Adoption for Music NFTs
paragraph — The first thing the music industry needs is more exposure. For artists, listeners and yeah, the labels. Even with the use cases mentioned above, the majority of the music industry still sees NFTs as a novelty rather than a legitimate way to run a business. We see a future where the experience is built and monetized on the blockchain, with labels taking part of the experience, as well.
Second, there needs to be a big jump in user experience. Listeners know what to expect with Spotify and Apple Music: a smooth, intuitive experience that lets them listen to Lil Nas X with just a few clicks. Web3 platforms aren’t quite there. Music NFTs and related premium content require extra steps that most people don’t yet have an appetite for.
H3 - How Could Music NFTs Save Artists?
paragraph — Musician Daniel Allan spent months building a relationship with the NFT community and raised 50 ETH to fund his new album, Overstimulated. Companies like Audius and artists like Vérité's, who raised $90,000 in an NFT launch, are at the forefront of exploring new ways to get paid. Avenged Sevenfold launched an NFT collection called "Deathbats Club" with 10,000 items that grants holders access to benefits such as meet and greets at shows, lifetime free tickets, limited edition merchandise, and more.

H4 - Static and dynamic content editing
A rich text element can be used with static or dynamic content. For static content, just drop it into any page and begin editing. For dynamic content, add a rich text field to any collection and then connect a rich text element to that field in the settings panel. Voila!
H5 — How to customize formatting for each rich text
Headings, paragraphs, blockquotes, figures, images, and figure captions can all be styled after a class is added to the rich text element using the "When inside of" nested selector system.
LINK — This is how a link looks like. Please provide normal & hover state (if different than this)
- This will be bullet points
- Numbered list is the same but with numbers
- It has a margin-left applied
- Headings, paragraphs, blockquotes, figures, images, and figure captions can all be styled after a class is added to the rich text element using the "When inside of" nested selector system.
- Headings, paragraphs, blockquotes, figures, images, and figure captions can all be styled after a class is added to the rich text element using the "When inside of" nested selector system.
- Headings, paragraphs, blockquotes, figures, images, and figure captions can all be styled after a class is added to the rich text element using the "When inside of" nested selector system.
QUOTE — Everyone is obsessed with making money and seeking alpha, which does a disservice to what [NFTs] can actually do. We have been instructing many bands that NFTs are a ticket for access to an exclusive club.” - M. Shadows, Avenged Sevenfold’s lead singer.