If you love Remix & Supabase this is the article for you. Supabase is an open-source Firebase alternative and it's one of my favorite go-to tools for developing applications. I have been using it for a while and I love it!
In today's article, I take you through a very simple image upload, because Supabase & Remix are so focused on developer experience, it's a breeze to get your images from the user's PC into a bucket on Supabase!
If you want to go into detail on everything Supabase has under its toolbelt to image processing and uploads you can find it here:
https://supabase.com/storage
Setup
Before we get to the good part let's just go through the setup first, I will assume you already have a Remix app set up so I won't go into that part, well let's install the dependencies:
$ npm i @supabase/supabase-js
Create a .env
file and add the following:
SUPABASE_TOKEN="<your-supabase-token>"
SUPABASE_URL="<your-supabase-url>"
SUPABASE_BUCKET="<name-of-your-bucket>"
SUPABASE_BUCKET_URL="<url-to-your-bucket>"
You can find more info here:
Supabase client
Alright, so before we get into the image uploads let's first create our Supabase client, we just add supabase.server.ts
file. For simplicity of this walkthrough, I will be adding it into my /app
directory, but you can add it wherever you like, and then inside of it paste in the following code:
import { createClient } from "@supabase/supabase-js";
// I use the ! to mark the env vars as defined but you should use some
// sort of validation to make sure they are!
// This creates our Supabase client, you can do many things with it!
export const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_TOKEN!
);
// This creates an utility for us to directly work with the bucket when needed
export const supabaseBucket = supabase.storage.from(
process.env.SUPABASE_BUCKET!
);
Well, we now have an officially working Supabase connection that we can use to do many cool things, one of which is image uploading! Let's see how we do that in Remix.
Uploading the image from the client
I will first create a simple ImageUploader
component that you can easily use to upload images:
import { Form } from "@remix-run/react";
import { useRef } from "react";
const ImageUploader = () => {
const inputRef = useRef<HTMLInputElement>(null);
return (
<>
<Form onChange={e => e.currentTarget.submit()} encType="multipart/form-data" method="post">
<input style={{ display: "none" }} name="image" ref={inputRef} type="file" />
</Form>
<button onClick={() => inputRef.current?.click()}>Upload Image</button>
</>
);
};
export { ImageUploader };
This is just a simple utility, we have a Form that creates a POST request to the current route as soon as you select your image. The form element listens to changes in the input and submits accordingly. The button just opens up the OS file picker so you can select an image. This can be changed to submit to a resource route for example, you can also add specific file types to the input, or you can use a third-party library, it's really up to you how you handle it on the client.
Alright, Now I go to the route I want to submit to, in my case I will use the _index.tsx
route. In there, I paste in the following code:
export default function Index() {
return (
<div>
<ImageUploader />
</div>
);
}
You should see the following on your screen:
Alright, we have our fully working client-side image uploader, now onto the server part! Obviously, this looks like it's 1980 and we're just getting into development and exploring the power of HTML but bear with me.
Server-side uploads
The first thing we need to do is parse the form data received from the client. At the moment of writing this article, Remix still considers parseMultipartFormData
as unstable, but we will be using this to handle our image parsing, let's go to the supabase.server.ts
file and paste in the following at the bottom:
export const supabaseUploadHandler =
(path: string): UploadHandler =>
async ({ data, filename }) => {
const chunks = [];
for await (const chunk of data) {
chunks.push(chunk);
}
const buffer = Buffer.concat(chunks);
// If there's no filename, it's a text field and we can return the value directly
if (!filename) {
const textDecoder = new TextDecoder();
return textDecoder.decode(buffer);
}
// Otherwise, it's an image and we'll save it to Supabase
const { data: image, error } = await supabase.storage
.from(process.env.SUPABASE_BUCKET!)
.upload(path, buffer, { upsert: true });
if (error || !image) {
// TODO Add error handling
console.log(error);
return null;
}
return image.path;
};
// Used to retrieve the image public url from Supabase
export const getImageUrl = (path: string) => {
return supabaseBucket.getPublicUrl(path).data.publicUrl;
};
Remix docs for reference:
https://remix.run/docs/en/main/utils/parse-multipart-form-data
Alright, this looks scary! What the heck does it do??
Well, it's simpler than you might think!
As the data is being streamed from the client we need a handler to read that data and do something with it, the following code snippet:
const chunks = [];
for await (const chunk of data) {
chunks.push(chunk);
}
const buffer = Buffer.concat(chunks);
Helps us by converting our incoming stream of data into a Buffer, this has the following uses:
The buffer can be sent further to Supabase
It can be converted back to a regular string
It is important to understand that the upload handler gets ALL the form data you send to the server, so if you have fields along the image you will need to parse those as well. This is where the following code snippet comes into play:
// If there's no filename, it's a text field and we can return the value directly
if (!filename) {
const textDecoder = new TextDecoder();
return textDecoder.decode(buffer);
}
This will make sure that we return any strings that come in as form data as strings and we don't do any transforms on them. But obviously, for our images, we want to do something more, so this is where this code snippet kicks in:
// Otherwise, it's an image and we'll save it to Supabase
const { data: image, error } = await supabase.storage
.from(process.env.SUPABASE_BUCKET!)
.upload(path, buffer);
This will use the buffer we created and try to upload the image to Supabase, the helper method here will store it in the bucket we specified in our .env
file and into a path we provided.
If something goes wrong it will return an error object, I leave the error handling up to you. Finally, after the image has been uploaded we return the url
to be stored in our DB (or wherever else).
Well now all that is left to do is test this out in our Remix app, let's go back to that route we created and submit an image, paste the following code in:
import {
ActionFunctionArgs,
unstable_parseMultipartFormData,
} from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { ImageUploader } from "~/components/ImageUploader";
import { getImageUrl, supabaseUploadHandler } from "~/supabase.server";
// We just retrieve the image by providing the same path as below,
// obviously this is for demo purposes and this would be smarter
// in your application
export const loader = () => {
// Gets the public image url so we can show it
return { imageUrl: getImageUrl("profile.jpg") };
};
export const action = async ({ request }: ActionFunctionArgs) => {
const formData = await unstable_parseMultipartFormData(
request,
supabaseUploadHandler("profile.jpg")
);
console.log(formData.get("image"));
return null;
};
export default function Index() {
const { imageUrl } = useLoaderData<typeof loader>();
return (
<div>
<img src={imageUrl} />
<ImageUploader />
</div>
);
}
And now if we upload an image we will be able to see it on screen:
There you have it! Full file upload to Supabase! Now there are a lot of things you can do from this point, like determining file extensions, validating image extensions, deleting the images via the Supabase API and so on. The rest I leave up to you!
Thank you
If you've reached the end you're a champ! I hope you liked my article.
If you wish to support me follow me on Twitter here:
or if you want to follow my work you can do so on GitHub:
And you can also sign up for the newsletter to get notified whenever I publish something new!