Server-side Setup

Next.js has a limit of how large a file can be passed through the built in API routes, if you need to enable larger uploads follow the client side setup guide

Start up Next.js Project

As with any Next.js project we can start one up with the following command

npx create-next-app@14

After the project is created cd into the repo and install pinata

npm i pinata-web3

After making the project, create a .env.local file in the root of the project and put in the following variables:

PINATA_JWT=
NEXT_PUBLIC_GATEWAY_URL=

Use the JWT from the API key creation in the previous step as well as the Gateway Domain. The format of the Gateway domain should be mydomain.mypinata.cloud.

Setup Pinata

Create a directory called utils in the root of the project and then make a file called config.ts inside of it. In that file we’ll export an instance of the IPFS SDK that we can use throughout the rest of the app.

utils/config.ts
"server only"

import { PinataSDK } from "pinata-web3"

export const pinata = new PinataSDK({
  pinataJwt: `${process.env.PINATA_JWT}`,
  pinataGateway: `${process.env.NEXT_PUBLIC_GATEWAY_URL}`
})

Create Client Side Form

Next we’ll want to make an upload form on the client side that will allow someone to select a file and upload it.

In the /app/page.tsx file take out the boiler plate code and use the following.

app/page.tsx
"use client";

import { useState } from "react";

export default function Home() {
  const [file, setFile] = useState<File>();
  const [url, setUrl] = useState("");
  const [uploading, setUploading] = useState(false);

  const uploadFile = async () => {
    try {
      if (!file) {
        alert("No file selected");
        return;
      }

      setUploading(true);
      const data = new FormData();
      data.set("file", file);
      const uploadRequest = await fetch("/api/files", {
        method: "POST",
        body: data,
      });
      const ipfsUrl = await uploadRequest.json();
      setUrl(ipfsUrl);
      setUploading(false);
    } catch (e) {
      console.log(e);
      setUploading(false);
      alert("Trouble uploading file");
    }
  };

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setFile(e.target?.files?.[0]);
  };

  return (
    <main className="w-full min-h-screen m-auto flex flex-col justify-center items-center">
      <input type="file" onChange={handleChange} />
      <button type="button" disabled={uploading} onClick={uploadFile}>
        {uploading ? "Uploading..." : "Upload"}
      </button>
    </main>
  );
}

This will take a file from the client side and upload it through an API route we are going to make next.

Next.js does have a file size limitation for what can be passed through the API routes, so if you need more than the limit then it is advised to make signed JWTs by folloing this guide.

Create API Route

Next.js is ideal for file uploads as it’s API routes keep keys hidden and unexposed to the client. In the last step we made a function that uploads to /api/files so now we need to create that route by making /app/api/files/route.ts in our app.

Once you have created that file you can paste in the following code.

app/api/files/route.ts
import { NextResponse, type NextRequest } from "next/server";
import { pinata } from "@/utils/config"

export async function POST(request: NextRequest) {
  try {
    const data = await request.formData();
    const file: File | null = data.get("file") as unknown as File;
    const uploadData = await pinata.upload.file(file)
    const url = await pinata.gateways.convert(uploadData.IpfsHash)
    return NextResponse.json(url, { status: 200 });
  } catch (e) {
    console.log(e);
    return NextResponse.json(
      { error: "Internal Server Error" },
      { status: 500 }
    );
  }
}

This will accept a POST request from the client, then send an API request to Pinata with the upload, then make one more request to get a signed URL we can use to see the content. Once complete it will return the URL to the client.

With our URL we can render the image we uploaded by adding the following code to the page.tsx file.

app/page.tsx
    return (
		<main className="w-full min-h-screen m-auto flex flex-col justify-center items-center">
			<input type="file" onChange={handleChange} />
			<button type="button" disabled={uploading} onClick={uploadFile}>
				{uploading ? "Uploading..." : "Upload"}
			</button>
			{/* Add a conditional looking for the signed url and use it as the source */}
			{url && <img src={url} alt="Image from Pinata" />}
		</main>
	);

And just like that we have uploaded an image to Pinata and recieved a usable URL in return!

Client Side Setup

Next.js has a file size limit as to what can be pass through API routes, so another workaround is to upload the file on the client side. To do this securely you can make an API route that generates a temporary Pinata JWT that is used in the upload.

Start up Next.js Project

As with any Next.js project we can start one up with the following command

npx create-next-app@14

After the project is created cd into the repo and install pinata

npm i pinata-web3

After making the project, create a .env.local file in the root of the project and put in the following variables:

PINATA_JWT=
NEXT_PUBLIC_GATEWAY_URL=

Use the JWT from the API key creation in the previous step as well as the Gateway Domain. The format of the Gateway domain should be mydomain.mypinata.cloud.

Setup Pinata

Create a directory called utils in the root of the project and then make a file called config.ts inside of it. In that file we’ll export an instance of the IPFS SDK that we can use throughout the rest of the app.

utils/config.ts
"server only"

import { PinataSDK } from "pinata-web3"

export const pinata = new PinataSDK({
  pinataJwt: `${process.env.PINATA_JWT}`,
  pinataGateway: `${process.env.NEXT_PUBLIC_GATEWAY_URL}`
})

Create API Route

In order to upload on the client side we need an API key, but we don’t want to expose our primary admin key. To avoid this we’ll make an API route in our Next project under app/api/key/route.ts.

Once you have created that file you can paste in the following code.

app/api/key/route.ts
import { NextResponse } from "next/server";
import { pinata } from "@/utils/config"

export const dynamic = "force-dynamic";

export async function GET() {
  try {
    const uuid = crypto.randomUUID();
    const keyData = await pinata.keys.create({
      keyName: uuid.toString(),
      permissions: {
        endpoints: {
          pinning: {
            pinFileToIPFS: true,
          },
        },
      },
      maxUses: 1,
    })
    return NextResponse.json(keyData, { status: 200 });
  } catch (error) {
    console.log(error);
    return NextResponse.json({ text: "Error creating API Key:" }, { status: 500 });
  }
}

When the client makes a GET request to /api/key it will return a temporary API key that is only valid for pinFileToIPFS and one single use (maxUses: 1).

Create Client Side Form

Next we’ll want to make an upload form on the client side that will allow someone to select a file and upload it with a temporary key

In the /app/page.tsx file take out the boiler plate code and use the following.

app/page.tsx
"use client";

import { useState } from "react";
import { pinata } from "@/utils/config";

export default function Home() {
  const [file, setFile] = useState<File>();
  const [uploading, setUploading] = useState(false);

  const uploadFile = async () => {
    if (!file) {
      alert("No file selected");
      return;
    }

    try {
      setUploading(true);
      const keyRequest = await fetch("/api/key");
      const keyData = await keyRequest.json();
      const upload = await pinata.upload.file(file).key(keyData.JWT);
      console.log(upload);
      setUploading(false);
    } catch (e) {
      console.log(e);
      setUploading(false);
      alert("Trouble uploading file");
    }
  };

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setFile(e.target?.files?.[0]);
  };

  return (
    <main className="w-full min-h-screen m-auto flex flex-col justify-center items-center">
      <input type="file" onChange={handleChange} />
      <button type="button" disabled={uploading} onClick={uploadFile}>
        {uploading ? "Uploading..." : "Upload"}
      </button>
    </main>
  );
}

The main thing to understand here is that we are able to use the key() method in combination with our upload methods which passes in the temporary key instead of trying to access the admin key.

Now that we have the file uploaded we can create a URL to display in our app.

app/page.tsx
"use client";

import { useState } from "react";
import { pinata } from "@/utils/config";

export default function Home() {
	const [file, setFile] = useState<File>();
	const [url, setUrl] = useState("");
	const [uploading, setUploading] = useState(false);

	const uploadFile = async () => {
		if (!file) {
			alert("No file selected");
			return;
		}

		try {
			setUploading(true);
			const keyRequest = await fetch("/api/key");
			const keyData = await keyRequest.json();
			const upload = await pinata.upload.file(file).key(keyData.JWT);
			const ipfsUrl = await pinata.gateways.convert(upload.IpfsHash)
			setUrl(ipfsUrl);
			setUploading(false);
		} catch (e) {
			console.log(e);
			setUploading(false);
			alert("Trouble uploading file");
		}
	};

	const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
		setFile(e.target?.files?.[0]);
	};

	return (
		<main className="w-full min-h-screen m-auto flex flex-col justify-center items-center">
			<input type="file" onChange={handleChange} />
			<button type="button" disabled={uploading} onClick={uploadFile}>
				{uploading ? "Uploading..." : "Upload"}
			</button>
			{url && <img src={url} alt="Image from Pinata" />}
		</main>
	);
}