From eb06f44f11efbedd223a1b24b6a359314c7837fd Mon Sep 17 00:00:00 2001
From: Anish Prashun <66175058+Anishpras@users.noreply.github.com>
Date: Fri, 26 Sep 2025 20:38:30 +0530
Subject: [PATCH 1/3] fix: separate server and client Pinata configs to
prevent JWT exposure
Fix Next.js documentation to properly separate server and client configurations for Pinata SDK.
The current documentation incorrectly shows importing server-side config (containing JWT) into client components,
which causes "Attempted to access a server-side environment variable on the client" errors.
Changes:
- Create separate server-config.ts (with JWT) and client-config.ts (without JWT)
- Update client-side setup to use client-config that only includes the public gateway URL
- Add warning about never exposing JWT with NEXT_PUBLIC prefix
- Clarify that client authentication happens through signed URLs from API routes, not direct JWT usage
This prevents accidental exposure of sensitive API keys to the client and fixes the runtime error users encounter
when following the current documentation.
---
frameworks/next-js.mdx | 582 ++++++++++++++++++++---------------------
1 file changed, 289 insertions(+), 293 deletions(-)
diff --git a/frameworks/next-js.mdx b/frameworks/next-js.mdx
index 9ebb3b4..0cb72ca 100644
--- a/frameworks/next-js.mdx
+++ b/frameworks/next-js.mdx
@@ -1,370 +1,366 @@
----
-title: "Next.js"
-description: "Get started using Pinata with Next.js"
-icon: "n"
----
+ ---
+ title: "Next.js"
+ description: "Get started using Pinata with Next.js"
+ icon: "n"
+ ---
-This guide will walk you through setting up Pinata in a Next.js app.
+ This guide will walk you through setting up Pinata in a Next.js app.
-## Create an API Key and get Gateway URL
+ ## Create an API Key and get Gateway URL
-To create an API key, visit the [Keys Page](https://app.pinata.cloud/developers/keys) and click the "New Key" button in the top right. Once you do that you can select if you want your key to be admin or if you want to scope the privileges of the keys to certain endpoints or limit the number of uses. Make those selections, then give the key a name at the bottom, and click create key.
+ To create an API key, visit the [Keys Page](https://app.pinata.cloud/developers/keys) and click the "New Key" button
+ in the top right. Once you do that you can select if you want your key to be admin or if you want to scope the
+ privileges of the keys to certain endpoints or limit the number of uses. Make those selections, then give the key a
+ name at the bottom, and click create key.
-
- If you are just getting started we recommend using Admin privileges, then move
- to scope keys as you better understand your needs
-
+
+ If you are just getting started we recommend using Admin privileges, then move
+ to scope keys as you better understand your needs
+
-
+
-Once you have created the keys you will be shown your API Key Info. This will contain your **Api Key**, **API Secret**, and your **JWT**. Click "Copy All" and save them somewhere safe!
+ Once you have created the keys you will be shown your API Key Info. This will contain your **Api Key**, **API
+ Secret**, and your **JWT**. Click "Copy All" and save them somewhere safe!
-
- The API keys are only shown once, be sure to copy them somewhere safe!
-
+
+ The API keys are only shown once, be sure to copy them somewhere safe!
+
-After you have your API key, you will want to get your Gateway domain. When you create a Pinata account, you'll automatically have a Gateway created for you! To see it, simply visit the [Gateways Page](https://app.pinata.cloud/gateway) see it listed there.
+ After you have your API key, you will want to get your Gateway domain. When you create a Pinata account, you'll
+ automatically have a Gateway created for you! To see it, simply visit the [Gateways
+ Page](https://app.pinata.cloud/gateway) see it listed there.
-
+
-The gateway domains are randomly generated and might look something like this:
+ The gateway domains are randomly generated and might look something like this:
-```
-aquamarine-casual-tarantula-177.mypinata.cloud
-```
+ aquamarine-casual-tarantula-177.mypinata.cloud
-## Server-Side Setup
+ ## 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
-
+
+ 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
+ ### Start up Next.js Project
-As with any Next.js project we can start one up with the following command
+ As with any Next.js project we can start one up with the following command
-```bash
-npx create-next-app@latest
-```
+ ```bash
+ npx create-next-app@latest
-After the project is created `cd` into the repo and install `pinata`
+ After the project is created cd into the repo and install pinata
-```bash
-npm i pinata
-```
+ npm i pinata
-After making the project, create a `.env.local` file in the root of the project and put in the following variables:
+ 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=
-```
+ 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`.
+ 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
+ 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 Files SDK that we can use throughout the rest of the app.
+ 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 Files SDK that we can use throughout the rest of the app.
-```typescript utils/config.ts
-"server only"
+ "server only"
-import { PinataSDK } from "pinata"
+ import { PinataSDK } from "pinata"
-export const pinata = new PinataSDK({
- pinataJwt: `${process.env.PINATA_JWT}`,
- pinataGateway: `${process.env.NEXT_PUBLIC_GATEWAY_URL}`
-})
-```
+ export const pinata = new PinataSDK({
+ pinataJwt: `${process.env.PINATA_JWT}`,
+ pinataGateway: `${process.env.NEXT_PUBLIC_GATEWAY_URL}`
+ })
-### Create Client Side Form
+ 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.
+ 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.
+ In the /app/page.tsx file take out the boiler plate code and use the following.
-```typescript app/page.tsx
-"use client";
+ "use client";
-import { useState } from "react";
+ import { useState } from "react";
-export default function Home() {
- const [file, setFile] = useState();
- const [url, setUrl] = useState("");
- const [uploading, setUploading] = useState(false);
+ export default function Home() {
+ const [file, setFile] = useState();
+ const [url, setUrl] = useState("");
+ const [uploading, setUploading] = useState(false);
- const uploadFile = async () => {
- try {
- if (!file) {
- alert("No file selected");
- return;
+ 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 signedUrl = await uploadRequest.json();
+ setUrl(signedUrl);
+ setUploading(false);
+ } catch (e) {
+ console.log(e);
+ setUploading(false);
+ alert("Trouble uploading file");
}
+ };
- setUploading(true);
- const data = new FormData();
- data.set("file", file);
- const uploadRequest = await fetch("/api/files", {
- method: "POST",
- body: data,
- });
- const signedUrl = await uploadRequest.json();
- setUrl(signedUrl);
- setUploading(false);
- } catch (e) {
- console.log(e);
- setUploading(false);
- alert("Trouble uploading file");
- }
- };
-
- const handleChange = (e: React.ChangeEvent) => {
- setFile(e.target?.files?.[0]);
- };
-
- return (
-
-
-
-
- );
-}
-```
+ const handleChange = (e: React.ChangeEvent) => {
+ setFile(e.target?.files?.[0]);
+ };
-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 following [this
- guide](https://www.pinata.cloud/blog/how-to-upload-to-ipfs-from-the-frontend-with-signed-jwts).
-
-
-### 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.
-
-```typescript 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 { cid } = await pinata.upload.public.file(file)
- const url = await pinata.gateways.public.convert(cid);
- return NextResponse.json(url, { status: 200 });
- } catch (e) {
- console.log(e);
- return NextResponse.json(
- { error: "Internal Server Error" },
- { status: 500 }
+ return (
+
+
+
+
);
}
-}
-```
-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.
+ This will take a file from the client side and upload it through an API route we are going to make next.
-With our URL we can render the image we uploaded by adding the following code to the `page.tsx` file.
+ Create API Route
-```typescript app/page.tsx
- return (
-
-
-
- {/* Add a conditional looking for the signed url and use it as the source */}
- {url && }
-
- );
-```
+ 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.
-And just like that we have uploaded an image to Pinata and recieved a usable URL in return!
+ Once you have created that file you can paste in the following code.
-## Client-Side Setup
+ import { NextResponse, type NextRequest } from "next/server";
+ import { pinata } from "@/utils/config"
-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 upload URL that is used in the upload request.
+ export async function POST(request: NextRequest) {
+ try {
+ const data = await request.formData();
+ const file: File | null = data.get("file") as unknown as File;
+ const { cid } = await pinata.upload.public.file(file)
+ const url = await pinata.gateways.public.convert(cid);
+ return NextResponse.json(url, { status: 200 });
+ } catch (e) {
+ console.log(e);
+ return NextResponse.json(
+ { error: "Internal Server Error" },
+ { status: 500 }
+ );
+ }
+ }
-### Start up Next.js Project
+ 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.
-As with any Next.js project we can start one up with the following command
+ With our URL we can render the image we uploaded by adding the following code to the page.tsx file.
-```bash
-npx create-next-app@latest
-```
+ return (
+
+
+
+ {/* Add a conditional looking for the signed url and use it as the source */}
+ {url && }
+
+ );
-After the project is created `cd` into the repo and install `pinata`
+ And just like that we have uploaded an image to Pinata and recieved a usable URL in return!
-```bash
-npm i pinata
-```
+ Client-Side Setup
-After making the project, create a `.env.local` file in the root of the project and put in the following variables:
+ 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 upload URL that is used
+ in the upload request.
-```
-PINATA_JWT=
-NEXT_PUBLIC_GATEWAY_URL=
-```
+ Start up Next.js Project
-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`.
+ As with any Next.js project we can start one up with the following command
-### Setup Pinata
+ npx create-next-app@latest
-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 Files SDK that we can use throughout the rest of the app.
+ After the project is created cd into the repo and install pinata
-```typescript utils/config.ts
-"server only"
+ npm i pinata
-import { PinataSDK } from "pinata"
+ After making the project, create a .env.local file in the root of the project and put in the following variables:
-export const pinata = new PinataSDK({
- pinataJwt: `${process.env.PINATA_JWT}`,
- pinataGateway: `${process.env.NEXT_PUBLIC_GATEWAY_URL}`
-})
-```
+ PINATA_JWT=
+ NEXT_PUBLIC_GATEWAY_URL=
-### Create API Route
+ 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.
-In order to upload on the client side we need to upload it securely without leaking our admin API key. To avoid this we'll make an API route in our Next project under `app/api/url/route.ts`.
+ Setup Pinata
-Once you have created that file you can paste in the following code.
+ Create a directory called utils in the root of the project and then make two config files - one for server-side
+ operations and one for client-side operations.
-```typescript app/api/url/route.ts
-import { NextResponse } from "next/server";
-import { pinata } from "@/utils/config"
+ First, create the server config file utils/server-config.ts:
-export const dynamic = "force-dynamic";
+ "server only"
-export async function GET() {
- // If you're going to use auth you'll want to verify here
- try {
- const url = await pinata.upload.public.createSignedURL({
- expires: 30, // The only required param
- })
- return NextResponse.json({ url: url }, { status: 200 }); // Returns the signed upload URL
- } catch (error) {
- console.log(error);
- return NextResponse.json({ text: "Error creating API Key:" }, { status: 500 });
- }
-}
-```
+ import { PinataSDK } from "pinata"
-When the client makes a `GET` request to `/api/url` it will return a temporary signed upload URL that is only valid for 30 seconds, which we can use on the client to make the upload request.
+ export const pinata = new PinataSDK({
+ pinataJwt: `${process.env.PINATA_JWT}`,
+ pinataGateway: `${process.env.NEXT_PUBLIC_GATEWAY_URL}`
+ })
-### Create Client Side Form
+ Then, create the client config file utils/client-config.ts:
-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 the signed upload URL
+ import { PinataSDK } from "pinata"
-In the `/app/page.tsx` file take out the boiler plate code and use the following.
+ export const pinata = new PinataSDK({
+ pinataGateway: `${process.env.NEXT_PUBLIC_GATEWAY_URL}`
+ })
-```typescript app/page.tsx
-"use client";
+ Create API Route
-import { useState } from "react";
-import { pinata } from "@/utils/config";
+ In order to upload on the client side we need to upload it securely without leaking our admin API key. To avoid this
+ we'll make an API route in our Next project under app/api/url/route.ts.
-export default function Home() {
- const [file, setFile] = useState();
- const [uploading, setUploading] = useState(false);
+ Once you have created that file you can paste in the following code.
- const uploadFile = async () => {
- if (!file) {
- alert("No file selected");
- return;
- }
+ import { NextResponse } from "next/server";
+ import { pinata } from "@/utils/server-config"
+ export const dynamic = "force-dynamic";
+
+ export async function GET() {
+ // If you're going to use auth you'll want to verify here
try {
- setUploading(true);
- const urlRequest = await fetch("/api/url"); // Fetches the temporary upload URL
- const urlResponse = await urlRequest.json(); // Parse response
- const upload = await pinata.upload.public
- .file(file)
- .url(urlResponse.url); // Upload the file with the signed URL
- console.log(upload);
- setUploading(false);
- } catch (e) {
- console.log(e);
- setUploading(false);
- alert("Trouble uploading file");
+ const url = await pinata.upload.public.createSignedURL({
+ expires: 30, // The only required param
+ })
+ return NextResponse.json({ url: url }, { status: 200 }); // Returns the signed upload URL
+ } catch (error) {
+ console.log(error);
+ return NextResponse.json({ text: "Error creating API Key:" }, { status: 500 });
}
- };
-
- const handleChange = (e: React.ChangeEvent) => {
- setFile(e.target?.files?.[0]);
- };
-
- return (
-
-
-
-
- );
-}
-```
+ }
+
+ When the client makes a GET request to /api/url it will return a temporary signed upload URL that is only valid for
+ 30 seconds, which we can use on the client to make the upload request.
+
+ 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
+ the signed upload URL
+
+ In the /app/page.tsx file take out the boiler plate code and use the following.
+
+ "use client";
+
+ import { useState } from "react";
+ import { pinata } from "@/utils/client-config";
-The main thing to understand here is that we are able to use the `url()` method in combination with our upload methods which passes in the signed upload url instead of trying to access the admin key. We can take the response and create a URL to access the file.
-
-```typescript app/page.tsx
-"use client";
-
-import { useState } from "react";
-import { pinata } from "@/utils/config";
-
-export default function Home() {
- const [file, setFile] = useState();
- const [url, setUrl] = useState("");
- const [uploading, setUploading] = useState(false);
-
- const uploadFile = async () => {
- if (!file) {
- alert("No file selected");
- return;
- }
-
- try {
- setUploading(true);
- const urlRequest = await fetch("/api/url"); // Fetches the temporary upload URL
- const urlResponse = await urlRequest.json(); // Parse response
- const upload = await pinata.upload.public
- .file(file)
- .url(urlResponse.url); // Upload the file with the signed URL
- const fileUrl = await pinata.gateways.public.convert(upload.cid)
- setUrl(fileUrl);
- setUploading(false);
- } catch (e) {
- console.log(e);
- setUploading(false);
- alert("Trouble uploading file");
- }
- };
-
- const handleChange = (e: React.ChangeEvent) => {
- setFile(e.target?.files?.[0]);
- };
-
- return (
-
-
-
- {url && }
-
- );
-}
+ export default function Home() {
+ const [file, setFile] = useState();
+ const [uploading, setUploading] = useState(false);
+
+ const uploadFile = async () => {
+ if (!file) {
+ alert("No file selected");
+ return;
+ }
+
+ try {
+ setUploading(true);
+ const urlRequest = await fetch("/api/url"); // Fetches the temporary upload URL
+ const urlResponse = await urlRequest.json(); // Parse response
+ const upload = await pinata.upload.public
+ .file(file)
+ .url(urlResponse.url); // Upload the file with the signed URL
+ console.log(upload);
+ setUploading(false);
+ } catch (e) {
+ console.log(e);
+ setUploading(false);
+ alert("Trouble uploading file");
+ }
+ };
+
+ const handleChange = (e: React.ChangeEvent) => {
+ setFile(e.target?.files?.[0]);
+ };
+
+ return (
+
+
+
+
+ );
+ }
+
+ The main thing to understand here is that we are able to use the url() method in combination with our upload methods
+ which passes in the signed upload url instead of trying to access the admin key. We can take the response and create
+ a URL to access the file.
+
+ "use client";
+
+ import { useState } from "react";
+ import { pinata } from "@/utils/client-config";
+
+ export default function Home() {
+ const [file, setFile] = useState();
+ const [url, setUrl] = useState("");
+ const [uploading, setUploading] = useState(false);
+
+ const uploadFile = async () => {
+ if (!file) {
+ alert("No file selected");
+ return;
+ }
+
+ try {
+ setUploading(true);
+ const urlRequest = await fetch("/api/url"); // Fetches the temporary upload URL
+ const urlResponse = await urlRequest.json(); // Parse response
+ const upload = await pinata.upload.public
+ .file(file)
+ .url(urlResponse.url); // Upload the file with the signed URL
+ const fileUrl = await pinata.gateways.public.convert(upload.cid)
+ setUrl(fileUrl);
+ setUploading(false);
+ } catch (e) {
+ console.log(e);
+ setUploading(false);
+ alert("Trouble uploading file");
+ }
+ };
+
+ const handleChange = (e: React.ChangeEvent) => {
+ setFile(e.target?.files?.[0]);
+ };
+
+ return (
+
+
+
+ {url && }
+
+ );
+ }
```
From 79149d21e339b0f7b3596f2e6826bc1d70625295 Mon Sep 17 00:00:00 2001
From: anishpras
Date: Fri, 26 Sep 2025 20:51:52 +0530
Subject: [PATCH 2/3] Added more details
---
frameworks/next-js.mdx | 602 ++++++++++++++++++++++-------------------
1 file changed, 319 insertions(+), 283 deletions(-)
diff --git a/frameworks/next-js.mdx b/frameworks/next-js.mdx
index 0cb72ca..9eb3920 100644
--- a/frameworks/next-js.mdx
+++ b/frameworks/next-js.mdx
@@ -1,366 +1,402 @@
- ---
- title: "Next.js"
- description: "Get started using Pinata with Next.js"
- icon: "n"
- ---
+---
+title: "Next.js"
+description: "Get started using Pinata with Next.js"
+icon: "n"
+---
- This guide will walk you through setting up Pinata in a Next.js app.
+This guide will walk you through setting up Pinata in a Next.js app.
- ## Create an API Key and get Gateway URL
+## Create an API Key and get Gateway URL
- To create an API key, visit the [Keys Page](https://app.pinata.cloud/developers/keys) and click the "New Key" button
- in the top right. Once you do that you can select if you want your key to be admin or if you want to scope the
- privileges of the keys to certain endpoints or limit the number of uses. Make those selections, then give the key a
- name at the bottom, and click create key.
+To create an API key, visit the [Keys Page](https://app.pinata.cloud/developers/keys) and click the "New Key" button in the top right. Once you do that you can select if you want your key to be admin or if you want to scope the privileges of the keys to certain endpoints or limit the number of uses. Make those selections, then give the key a name at the bottom, and click create key.
-
- If you are just getting started we recommend using Admin privileges, then move
- to scope keys as you better understand your needs
-
+
+ If you are just getting started we recommend using Admin privileges, then move
+ to scope keys as you better understand your needs
+
-
+
- Once you have created the keys you will be shown your API Key Info. This will contain your **Api Key**, **API
- Secret**, and your **JWT**. Click "Copy All" and save them somewhere safe!
+Once you have created the keys you will be shown your API Key Info. This will contain your **Api Key**, **API Secret**, and your **JWT**. Click "Copy All" and save them somewhere safe!
-
- The API keys are only shown once, be sure to copy them somewhere safe!
-
+
+ The API keys are only shown once, be sure to copy them somewhere safe!
+
- After you have your API key, you will want to get your Gateway domain. When you create a Pinata account, you'll
- automatically have a Gateway created for you! To see it, simply visit the [Gateways
- Page](https://app.pinata.cloud/gateway) see it listed there.
+After you have your API key, you will want to get your Gateway domain. When you create a Pinata account, you'll automatically have a Gateway created for you! To see it, simply visit the [Gateways Page](https://app.pinata.cloud/gateway) see it listed there.
-
+
- The gateway domains are randomly generated and might look something like this:
+The gateway domains are randomly generated and might look something like this:
- aquamarine-casual-tarantula-177.mypinata.cloud
-
- ## Server-Side Setup
+```
+aquamarine-casual-tarantula-177.mypinata.cloud
+```
-
- 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
-
+## 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
+### Start up Next.js Project
- As with any Next.js project we can start one up with the following command
+As with any Next.js project we can start one up with the following command
- ```bash
- npx create-next-app@latest
+```bash
+npx create-next-app@latest
+```
- After the project is created cd into the repo and install pinata
+After the project is created `cd` into the repo and install `pinata`
- npm i pinata
+```bash
+npm i pinata
+```
- After making the project, create a .env.local file in the root of the project and put in the following variables:
+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=
+```
+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.
+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
+### 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 Files SDK that we can use throughout the rest of the app.
+
+ CRITICAL: Never prefix your JWT with `NEXT_PUBLIC_` as this will expose it to the client-side code and compromise your API security. The JWT should only be used in server-side code.
+
- "server only"
+Create a directory called `utils` in the root of the project and then make a file called `server-config.ts` inside of it. This file will contain your server-side Pinata configuration with the JWT for secure API operations.
- import { PinataSDK } from "pinata"
+```typescript utils/server-config.ts
+"use server";
- export const pinata = new PinataSDK({
- pinataJwt: `${process.env.PINATA_JWT}`,
- pinataGateway: `${process.env.NEXT_PUBLIC_GATEWAY_URL}`
- })
+import { PinataSDK } from "pinata";
- Create Client Side Form
+export const pinata = new PinataSDK({
+ pinataJwt: `${process.env.PINATA_JWT}`,
+ pinataGateway: `${process.env.NEXT_PUBLIC_GATEWAY_URL}`,
+});
+```
- Next we'll want to make an upload form on the client side that will allow someone to select a file and upload it.
+### Create Client Side Form
- In the /app/page.tsx file take out the boiler plate code and use the following.
+Next we'll want to make an upload form on the client side that will allow someone to select a file and upload it.
- "use client";
+In the `/app/page.tsx` file take out the boiler plate code and use the following.
- import { useState } from "react";
+```typescript app/page.tsx
+"use client";
- export default function Home() {
- const [file, setFile] = useState();
- const [url, setUrl] = useState("");
- const [uploading, setUploading] = useState(false);
+import { useState } from "react";
- const uploadFile = async () => {
- try {
- if (!file) {
- alert("No file selected");
- return;
- }
+export default function Home() {
+ const [file, setFile] = useState();
+ const [url, setUrl] = useState("");
+ const [uploading, setUploading] = useState(false);
- setUploading(true);
- const data = new FormData();
- data.set("file", file);
- const uploadRequest = await fetch("/api/files", {
- method: "POST",
- body: data,
- });
- const signedUrl = await uploadRequest.json();
- setUrl(signedUrl);
- setUploading(false);
- } catch (e) {
- console.log(e);
- setUploading(false);
- alert("Trouble uploading file");
+ const uploadFile = async () => {
+ try {
+ if (!file) {
+ alert("No file selected");
+ return;
}
- };
-
- const handleChange = (e: React.ChangeEvent) => {
- setFile(e.target?.files?.[0]);
- };
-
- return (
-
-
-
-
+
+ setUploading(true);
+ const data = new FormData();
+ data.set("file", file);
+ const uploadRequest = await fetch("/api/files", {
+ method: "POST",
+ body: data,
+ });
+ const signedUrl = await uploadRequest.json();
+ setUrl(signedUrl);
+ setUploading(false);
+ } catch (e) {
+ console.log(e);
+ setUploading(false);
+ alert("Trouble uploading file");
+ }
+ };
+
+ const handleChange = (e: React.ChangeEvent) => {
+ setFile(e.target?.files?.[0]);
+ };
+
+ return (
+
+
+
+
+ );
+}
+```
+
+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 following [this
+ guide](https://www.pinata.cloud/blog/how-to-upload-to-ipfs-from-the-frontend-with-signed-jwts).
+
+
+### 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.
+
+```typescript app/api/files/route.ts
+import { NextResponse, type NextRequest } from "next/server";
+import { pinata } from "@/utils/server-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 { cid } = await pinata.upload.public.file(file);
+ const url = await pinata.gateways.public.convert(cid);
+ return NextResponse.json(url, { status: 200 });
+ } catch (e) {
+ console.log(e);
+ return NextResponse.json(
+ { error: "Internal Server Error" },
+ { status: 500 }
);
}
+}
+```
- This will take a file from the client side and upload it through an API route we are going to make next.
+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.
+
+```typescript app/page.tsx
+return (
+
+
+
+ {/* Add a conditional looking for the signed url and use it as the source */}
+ {url && }
+
+);
+```
- Create API Route
+And just like that we have uploaded an image to Pinata and recieved a usable URL in return!
- 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.
+## Client-Side Setup
- Once you have created that file you can paste in the following code.
+
+ IMPORTANT: When uploading from the client side, never expose your JWT directly. Instead, use API routes to generate temporary signed URLs that the client can use for uploads. This keeps your JWT secure on the server.
+
- import { NextResponse, type NextRequest } from "next/server";
- import { pinata } from "@/utils/config"
+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 upload URL that is used in the upload request.
- export async function POST(request: NextRequest) {
- try {
- const data = await request.formData();
- const file: File | null = data.get("file") as unknown as File;
- const { cid } = await pinata.upload.public.file(file)
- const url = await pinata.gateways.public.convert(cid);
- return NextResponse.json(url, { status: 200 });
- } catch (e) {
- console.log(e);
- return NextResponse.json(
- { error: "Internal Server Error" },
- { status: 500 }
- );
- }
- }
+### Start up Next.js Project
- 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.
+As with any Next.js project we can start one up with the following command
- With our URL we can render the image we uploaded by adding the following code to the page.tsx file.
+```bash
+npx create-next-app@latest
+```
- return (
-
-
-
- {/* Add a conditional looking for the signed url and use it as the source */}
- {url && }
-
- );
+After the project is created `cd` into the repo and install `pinata`
- And just like that we have uploaded an image to Pinata and recieved a usable URL in return!
+```bash
+npm i pinata
+```
- Client-Side Setup
+After making the project, create a `.env.local` file in the root of the project and put in the following variables:
- 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 upload URL that is used
- in the upload request.
+```
+PINATA_JWT=
+NEXT_PUBLIC_GATEWAY_URL=
+```
- Start up Next.js Project
+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`.
- As with any Next.js project we can start one up with the following command
+### Setup Pinata
- npx create-next-app@latest
+
+ CRITICAL: Never prefix your JWT with `NEXT_PUBLIC_` as this will expose it to the client-side code and compromise your API security. The JWT should only be used in server-side code.
+
- After the project is created cd into the repo and install pinata
+Create a directory called `utils` in the root of the project and then make a file called `server-config.ts` inside of it. This file will contain your server-side Pinata configuration with the JWT for secure API operations.
- npm i pinata
+```typescript utils/server-config.ts
+"use server";
- After making the project, create a .env.local file in the root of the project and put in the following variables:
+import { PinataSDK } from "pinata";
- PINATA_JWT=
- NEXT_PUBLIC_GATEWAY_URL=
+export const pinata = new PinataSDK({
+ pinataJwt: `${process.env.PINATA_JWT}`,
+ pinataGateway: `${process.env.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.
+### Create API Route
- Setup Pinata
+In order to upload on the client side we need to upload it securely without leaking our admin API key. To avoid this we'll make an API route in our Next project under `app/api/url/route.ts`.
- Create a directory called utils in the root of the project and then make two config files - one for server-side
- operations and one for client-side operations.
+Once you have created that file you can paste in the following code.
- First, create the server config file utils/server-config.ts:
+```typescript app/api/url/route.ts
+import { NextResponse } from "next/server";
+import { pinata } from "@/utils/server-config";
- "server only"
+export const dynamic = "force-dynamic";
- import { PinataSDK } from "pinata"
+export async function GET() {
+ // If you're going to use auth you'll want to verify here
+ try {
+ const url = await pinata.upload.public.createSignedURL({
+ expires: 30, // The only required param
+ });
+ return NextResponse.json({ url: url }, { status: 200 }); // Returns the signed upload URL
+ } catch (error) {
+ console.log(error);
+ return NextResponse.json(
+ { text: "Error creating API Key:" },
+ { status: 500 }
+ );
+ }
+}
+```
- export const pinata = new PinataSDK({
- pinataJwt: `${process.env.PINATA_JWT}`,
- pinataGateway: `${process.env.NEXT_PUBLIC_GATEWAY_URL}`
- })
+When the client makes a `GET` request to `/api/url` it will return a temporary signed upload URL that is only valid for 30 seconds, which we can use on the client to make the upload request.
- Then, create the client config file utils/client-config.ts:
+### Create Client Configuration
- import { PinataSDK } from "pinata"
+Since the client-side code cannot access server-side environment variables (those without `NEXT_PUBLIC_` prefix), we need to create a separate client configuration file that only includes the public gateway URL.
- export const pinata = new PinataSDK({
- pinataGateway: `${process.env.NEXT_PUBLIC_GATEWAY_URL}`
- })
+```typescript utils/client-config.ts
+"use client";
- Create API Route
+import { PinataSDK } from "pinata";
- In order to upload on the client side we need to upload it securely without leaking our admin API key. To avoid this
- we'll make an API route in our Next project under app/api/url/route.ts.
+// Client-side config only uses the public gateway URL
+// Authentication happens through signed URLs from API routes, not direct JWT usage
+export const pinata = new PinataSDK({
+ pinataGateway: `${process.env.NEXT_PUBLIC_GATEWAY_URL}`,
+});
+```
- Once you have created that file you can paste in the following code.
+
+ The client configuration does NOT include the JWT. Client-side authentication is handled through signed URLs generated by your API routes, ensuring your JWT remains secure on the server.
+
- import { NextResponse } from "next/server";
- import { pinata } from "@/utils/server-config"
+### Create Client Side Form
- export const dynamic = "force-dynamic";
+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 the signed upload URL.
- export async function GET() {
- // If you're going to use auth you'll want to verify here
- try {
- const url = await pinata.upload.public.createSignedURL({
- expires: 30, // The only required param
- })
- return NextResponse.json({ url: url }, { status: 200 }); // Returns the signed upload URL
- } catch (error) {
- console.log(error);
- return NextResponse.json({ text: "Error creating API Key:" }, { status: 500 });
- }
- }
+In the `/app/page.tsx` file take out the boiler plate code and use the following.
+
+```typescript app/page.tsx
+"use client";
- When the client makes a GET request to /api/url it will return a temporary signed upload URL that is only valid for
- 30 seconds, which we can use on the client to make the upload request.
+import { useState } from "react";
+import { pinata } from "@/utils/client-config";
- Create Client Side Form
+export default function Home() {
+ const [file, setFile] = useState();
+ const [uploading, setUploading] = useState(false);
- 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
- the signed upload URL
+ const uploadFile = async () => {
+ if (!file) {
+ alert("No file selected");
+ return;
+ }
- In the /app/page.tsx file take out the boiler plate code and use the following.
+ try {
+ setUploading(true);
+ const urlRequest = await fetch("/api/url"); // Fetches the temporary upload URL from server
+ const urlResponse = await urlRequest.json(); // Parse response
+ const upload = await pinata.upload.public.file(file).url(urlResponse.url); // Upload the file with the signed URL
+ console.log(upload);
+ setUploading(false);
+ } catch (e) {
+ console.log(e);
+ setUploading(false);
+ alert("Trouble uploading file");
+ }
+ };
+
+ const handleChange = (e: React.ChangeEvent) => {
+ setFile(e.target?.files?.[0]);
+ };
+
+ return (
+
+
+
+
+ );
+}
+```
- "use client";
+The main thing to understand here is that we are able to use the `url()` method in combination with our upload methods which passes in the signed upload url instead of trying to access the admin key. We can take the response and create a URL to access the file.
- import { useState } from "react";
- import { pinata } from "@/utils/client-config";
+```typescript app/page.tsx
+"use client";
- export default function Home() {
- const [file, setFile] = useState();
- const [uploading, setUploading] = useState(false);
+import { useState } from "react";
+import { pinata } from "@/utils/client-config";
- const uploadFile = async () => {
- if (!file) {
- alert("No file selected");
- return;
- }
+export default function Home() {
+ const [file, setFile] = useState();
+ const [url, setUrl] = useState("");
+ const [uploading, setUploading] = useState(false);
- try {
- setUploading(true);
- const urlRequest = await fetch("/api/url"); // Fetches the temporary upload URL
- const urlResponse = await urlRequest.json(); // Parse response
- const upload = await pinata.upload.public
- .file(file)
- .url(urlResponse.url); // Upload the file with the signed URL
- console.log(upload);
- setUploading(false);
- } catch (e) {
- console.log(e);
- setUploading(false);
- alert("Trouble uploading file");
- }
- };
-
- const handleChange = (e: React.ChangeEvent) => {
- setFile(e.target?.files?.[0]);
- };
-
- return (
-
-
-
-
- );
- }
+ const uploadFile = async () => {
+ if (!file) {
+ alert("No file selected");
+ return;
+ }
- The main thing to understand here is that we are able to use the url() method in combination with our upload methods
- which passes in the signed upload url instead of trying to access the admin key. We can take the response and create
- a URL to access the file.
-
- "use client";
-
- import { useState } from "react";
- import { pinata } from "@/utils/client-config";
-
- export default function Home() {
- const [file, setFile] = useState();
- const [url, setUrl] = useState("");
- const [uploading, setUploading] = useState(false);
-
- const uploadFile = async () => {
- if (!file) {
- alert("No file selected");
- return;
- }
-
- try {
- setUploading(true);
- const urlRequest = await fetch("/api/url"); // Fetches the temporary upload URL
- const urlResponse = await urlRequest.json(); // Parse response
- const upload = await pinata.upload.public
- .file(file)
- .url(urlResponse.url); // Upload the file with the signed URL
- const fileUrl = await pinata.gateways.public.convert(upload.cid)
- setUrl(fileUrl);
- setUploading(false);
- } catch (e) {
- console.log(e);
- setUploading(false);
- alert("Trouble uploading file");
- }
- };
-
- const handleChange = (e: React.ChangeEvent) => {
- setFile(e.target?.files?.[0]);
- };
-
- return (
-
-
-
- {url && }
-
- );
- }
+ try {
+ setUploading(true);
+ const urlRequest = await fetch("/api/url"); // Fetches the temporary upload URL from server
+ const urlResponse = await urlRequest.json(); // Parse response
+ const upload = await pinata.upload.public.file(file).url(urlResponse.url); // Upload the file with the signed URL
+ const fileUrl = await pinata.gateways.public.convert(upload.cid);
+ setUrl(fileUrl);
+ setUploading(false);
+ } catch (e) {
+ console.log(e);
+ setUploading(false);
+ alert("Trouble uploading file");
+ }
+ };
+
+ const handleChange = (e: React.ChangeEvent) => {
+ setFile(e.target?.files?.[0]);
+ };
+
+ return (
+
+
+
+ {url && }
+
+ );
+}
```
From 592a8044e8b867690c70c90d390cc959c2bd2f22 Mon Sep 17 00:00:00 2001
From: anishpras
Date: Fri, 26 Sep 2025 20:53:52 +0530
Subject: [PATCH 3/3] line break resolved
---
frameworks/next-js.mdx | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/frameworks/next-js.mdx b/frameworks/next-js.mdx
index 9eb3920..5baa9c5 100644
--- a/frameworks/next-js.mdx
+++ b/frameworks/next-js.mdx
@@ -42,9 +42,7 @@ aquamarine-casual-tarantula-177.mypinata.cloud
## 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
+ 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