Back to blog

How to Upload Files with Astro and Pinata

How to Upload Files with Astro and Pinata

Steve

There are many Javascript/Typescript web frameworks out there, but Astro is special. If you’re not familiar with Astro, it’s a unique take on how to approach building web apps. It’s designed to be content driven with built-in features to handle markdown content, but also has an amazing plugin ecosystem. At any point, you can use components from a plethora of other frameworks, like React, Svelte, or Vue, and you can use them altogether in one app. It truly feels like magic, so of course, it’s natural that we needed a guide on how you can integrate file uploads using Pinata!

Setup

To get started, we’ll need to setup just a few things, including a Pinata account. If you don’t already have one, you can get one here for free! Once you’ve created an account, you’ll want to navigate to the API Keys tab on the left hand side. Make a New Key with the button in the top right and give it admin permissions with unlimited uses (you can scope the keys later on if you want to). Copy down all the API key information, but we’ll primarily be using the larger JWT provided. Last thing we need from the Pinata dashboard is our Gateway domain. You can get this by clicking on “Gateways” in the left side bar and copying down the URL just as you see it, which should be something like example-llama.mypinata.cloud.

Outside our Pinata account, all you need is Node.js installed (preferably v.20 or higher) and a good text editor. Open up your terminal and run this command to start up the Astro project repo.

npm create astro@latest pinata-astro

When it gives you the options to choose from - with it’s fun little Houston robot - we would recommend the following selections:

  tmpl   How would you like to start your new project?
         Empty

    ts   Do you plan to write TypeScript?
         Yes

   use   How strict should TypeScript be?
         Strict

  deps   Install dependencies?
         Yes

   git   Initialize a new git repository?
         Yes

Once it’s completed installing all the dependencies, go ahead and cd into the repo.

cd pinata-astro

From here, we get to pick some fun flavors of how our app will work! Like we mentioned earlier, you can use a variety of UI frameworks, as well as deployment options. For us, we’ll be using Svelte for our UI component, and we’ll be using Vercel to handle our server-side deployment. Feel free to explore the other frameworks and deployment methods and pick what works best for you! Installing them as plugins is crazy simple, just run the command below.

npx astro add vercel svelte

These will not only install the necessary packages, but also modify your Astro config to make sure everything runs smoothly. With both of those installed, we finally just need to add the Pinata SDK, which makes uploads smooth like butter. Run this command in the terminal to download it.

npm i pinata

Once it’s installed, we can go ahead and setup an instance of the Pinata SDK in the project! In the src folder, make a new folder called utils and put a file inside called pinata.ts. Then, let’s put in the following code.

import { PinataSDK } from "pinata";

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

This little file will export our Pinata SDK instance to wherever we need it, and it will use some environment variables that we need to setup now. Do this by making a file in the root of the project called .env and put in the following contents:

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 all of this setup, we can start creating our upload flow!

Implementing Uploads

To implement file uploads into our app, we’ll need two things: a client side form and a server side function to handle the upload like an API route. Let’s make the form using our Svelte integration by creating a new folder in src called components, and inside there make a file called UploadForm.svelte. Once you do that, paste in the code below.

<script lang="ts">
let url: string;
let isUploading: boolean;

async function submit(e: SubmitEvent) {
	isUploading = true;
	e.preventDefault();
	const formData = new FormData(e.currentTarget as HTMLFormElement);
	const request = await fetch("/api/upload", {
		method: "POST",
		body: formData,
	});
	const response = await request.json();
	url = response.data;
	isUploading = false;
}
</script>

<form on:submit={submit}>
  <input type="file" id="file" name="file" required />
  <button>{isUploading ? "Uploading..." : "Upload"}</button>
  {#if url}
    <img src={url} alt="pinnie" />
  {/if}
</form>

Our form component is made up of two parts, the <script> where our Typescript runs, and below it is our <form> markup. In the form, we have some Svelte syntax for things like on:submit to run a function when the form is submitted, as well as some conditional rendering for the <button> and <img> tags. In the script, we have just two state variables, the resulting URL that will be our image source, and a loading state of isUploading. The function simply takes the selected file and sends it as formData to our API route, then parses the result and sets the url state. That simple! Now, let’s go make that API route by making a folder inside src/pages called api, and then make a file in there called upload.ts. In the end, the full path should be src/pages/api/upload.ts. In that file, let’s paste in the following code:

import type { APIRoute } from "astro";
import { pinata } from "../../utils/pinata";

export const POST: APIRoute = async ({ request }) => {
	const data = await request.formData();
	const file = data.get("file") as File;
	if (!file) {
		return new Response(
			JSON.stringify({
				message: "Missing file",
			}),
			{ status: 400 },
		);
	}
	const { cid } = await pinata.upload.file(file);
	const url = await pinata.gateways.createSignedURL({
		cid: cid,
		expires: 360,
	});
	return new Response(
		JSON.stringify({
			data: url,
		}),
		{ status: 200 },
	);
};

In this API endpoint, we use some Astro types, but in reality it’s all just Typescript. Pretty simple and straight forward. We parse the incoming formData and get the file that we attached from our form submission. If there isn’t a file, then we can return an error to the user. Otherwise, we can make two simple methods using the Pinata SDK. The first one, we upload the file and get the cid as a deconstructed response, then we used that special file identifier to create a signed URL. All files uploaded to Pinata using the Files API are, by default, private, so using this method will get a temporary URL that can be used to view the content. With that URL created, we’ll just send it back to the client to be rendered in the <img> tag!

Last, but not least, we need to import our new component into the main index.astro file inside the pages directory.

---
import UploadForm from "../components/UploadForm.svelte";
---

<html lang="en">
	<head>
		<meta charset="utf-8" />
		<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
		<meta name="viewport" content="width=device-width" />
		<meta name="generator" content={Astro.generator} />
		<title>Astro</title>
	</head>
	<body>
		<h1>Astro + Pinata</h1>
		<UploadForm client:load />
	</body>
</html>

I just love how easy Astro makes it; import it at the top and put it into the HTML body! One important thing we added here is inside the <UploadForm /> component is the client:load attribute. By default, Astro will render all content as server side content, which makes it pretty fast. Anytime we need some client side functionality, we can just pass this in. Now, just run npm run dev in the terminal and you should be able to upload an image from the main page and see the results!

0:00
/0:09

Wrapping Up

This really is just the tip of the iceberg when it come to the possibilities of Pinata and Astro.

Need some inspiration? Be sure to check out some of our other tutorials as well as our docs. Of course don’t forget to get a free Pinata account that will scale with your app and services!

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.