Back to blog

How to Upload Files with Remix and Pinata

How to Upload Files with Remix and Pinata

Steve

If you read some of Pinata’s blogs and tutorials, you’ve probably noticed we use Next.js a fair bit. While it is an easy framework to build an MVP with, its not always the best choice for every job. OpenAI seemed to think the same thing when they migrated their ChatGPT web app from Next to Remix. While Remix isn’t a new framework by any means, it has certainly gained popularity in the past few months and for good reasons.

Remix is a full stack framework built on Vite, instead of Webpack, and has some unique features; including nested routes that render in parallel formation instead of a waterfall, which created a fast and smooth loading site. Additionally, the markup has a unique style where you can use a mix of React JSX, as well as traditional HTML form requests and responses. Like any full stack app, the chances are you are gonna need uploads at some point, and Pinata has you covered! In this tutorial, we’ll show you how to setup a Remix project and upload files with Pinata.

Setup

Before we start building uploads into Remix, we’ll need to setup just a few things.

Pinata

Of course, you will need a free Pinata account, which you can create in just a minute or two here. Once you’re in, you’ll want to create an API key, which you can do from the API Keys tab and then clicking New Key in the top right. Give it a name, admin permissions, and be sure to save the JWT which we’ll be using in just a bit. Then, you’ll want to grab the Gateway domain, which is included with your account from the Gateways tab, and it it should look something like aqua-keen-cobra-996.mypinata.cloud which you can copy down where you kept the JWT.

Remix

Getting started with Remix is a breeze; just make sure you have Node and NPM installed and a good text editor! Open up the terminal and run the following command:

npx create-remix@latest pinata-remix

From there, you can just select all the default options for setting up a git repo and installing dependencies. Once complete, run this command to cd into the repo and install the Pinata SDK

cd pinata-remix && npm i pinata

After that is done, you can open the project in your text editor and create a file in the root of the project called .env where you can put in the following variables:

PINATA_JWT= # The Pinata JWT API key we got earlier
GATEWAY_URL= # The Gateway domain we grabbed earlier, formatting just as we copied it from the app

With our environment variables secured, we can now create our Pinata SDK instance. Make a new folder in the root of the project called utils, and in there, make a file called config.ts and paste in the following code:

import { PinataSDK } from "pinata";

export const pinata = new PinataSDK({
	pinataJwt: process.env.PINATA_JWT,
	pinataGateway: process.env.GATEWAY_URL,
});

This simply creates an instance of the Pinata SDK we can use throughout our app, but we really only need it in one spot; let’s upload some files!

Upload a File

Remix has an interesting app structure where lots of the skeleton is exposed but you may never need to touch it. The only file we need to edit is under app/routes/_index.tsx. In there, we’ll take out the boilerplate code and replace it with this:

import type { MetaFunction } from "@remix-run/node";
import { Form, useActionData, useNavigation } from "@remix-run/react";

export const meta: MetaFunction = () => {
	return [
		{ title: "Remix + Pinata" },
		{ name: "description", content: "Upload files on Remix with Pinata!" },
	];
};

export default function Index() {
	const actionData = useActionData<typeof action>();
	const navigation = useNavigation();
	const isSubmitting = navigation.state === "submitting";

	return (
		<div className="font-sans p-4 flex flex-col gap-4 justify-center items-center min-h-screen max-w-[500px] mx-auto">
			<h1 className="text-3xl font-bold">Remix + Pinata</h1>
			<Form
				encType="multipart/form-data"
				method="post"
				className="flex flex-col gap-4"
			>
				<input type="file" name="file" className="" />
				<button
					className="bg-[#582CD6] text-white rounded-md p-2"
					type="submit"
				>
					{isSubmitting ? "Uploading..." : "Upload"}
				</button>
			</Form>
			{actionData?.url && (
				<div className="mt-4">
					<a
						href={actionData.url}
						target="_blank"
						rel="noreferrer"
						className="text-[#582CD6] underline"
					>
						{actionData.url}
					</a>
				</div>
			)}
		</div>
	);
}

This just give us some simple markup for the UI where we create a <Form/> component that has a method of post , an file input, and a submission button. What is really nice about Remix is that it works similar to old fashioned HTML forms where it sends the API request and form data; no messing with React state or any other stuff, just pure values. Now, one important thing we added in here for file uploads is this line in the Form component:

encType="multipart/form-data"

File uploads are still something Remix is working on and improving, and this newer encoding API really make it much simpler than it has been before. Huge shout out to David Allen who wrote about this just a few days ago and shared this with the Remix community!

We are also using a hook called useActionData, which lets us receive and use the data return from our form post request. In this case, we’re going to select a file, upload it, and our function will return a signed URL that gives us temporary access to the file. Speaking of that action, let’s add it to our file now:

// Update our Remix imports 
import type { ActionFunctionArgs, MetaFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { Form, useActionData, useNavigation } from "@remix-run/react";
// Brin in our Pinata SDK instance
import { pinata } from "utils/config";

export const meta: MetaFunction = () => {
	return [
		{ title: "Remix + Pinata" },
		{ name: "description", content: "Upload files on Remix with Pinata!" },
	];
};

// Server side action to handle our upload
export const action = async ({ request }: ActionFunctionArgs) => {
	const formData = await request.formData();
	const file = formData.get("file") as File;
	const { cid } = await pinata.upload.file(file);
	const url = await pinata.gateways.createSignedURL({
		cid: cid,
		expires: 60,
	});

	return json({ url });
};

export default function Index() {
	const actionData = useActionData<typeof action>();
	const navigation = useNavigation();
	const isSubmitting = navigation.state === "submitting";

	return (
		<div className="font-sans p-4 flex flex-col gap-4 justify-center items-center min-h-screen max-w-[500px] mx-auto">
			<h1 className="text-3xl font-bold">Remix + Pinata</h1>
			<Form
				encType="multipart/form-data"
				method="post"
				className="flex flex-col gap-4"
			>
				<input type="file" name="file" className="" />
				<button
					className="bg-[#582CD6] text-white rounded-md p-2"
					type="submit"
				>
					{isSubmitting ? "Uploading..." : "Upload"}
				</button>
			</Form>
			{actionData?.url && (
				<div className="mt-4">
					<a
						href={actionData.url}
						target="_blank"
						rel="noreferrer"
						className="text-[#582CD6] underline"
					>
						{actionData.url}
					</a>
				</div>
			)}
		</div>
	);
}

With just a few lines of extra code, we have our server side action, and there is where the post request from the form will be sent to by default; very smooth! All we have to do is use the incoming request to parse it as formData, get the attached file, then upload it with our Pinata SDK. Once it’s uploaded, we deconstruct the returned object and use the cid or “Content Identifier” to create a signed URL that is good for 60 seconds, then we just return the URL. If you go ahead and run npm run dev in the terminal, you should have a flow like this:

0:00
/0:09

Wrapping Up

There’s something oddly satisfying about having a full stack app in just one page of code, isn’t it? That’s the beauty of Remix and Pinata: simplifying your code and your uploads with first class performance and developer experience. Of course, this is just the beginning, so be sure to check out our docs and see what else is possible!

Subscribe to paid plan image

Share this post:

Stay up to date

Join our newsletter for the latest stories & product updates from the Pinata community.