Back to blog
How to Upload Files with Astro and Pinata
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!
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!