The Remix.run documentation shows us how to upload files directly to the disk of the application, although you have to look around a bit.

The code to upload a file to the /public/uploads directory looks like this.

// ./app/routes/upload.tsx

import {
ActionFunction,
Form,
unstable_createFileUploadHandler,
unstable_parseMultipartFormData,
} from 'remix';

export const fileUploadHandler = unstable_createFileUploadHandler({
directory: './public/uploads',
file: ({ filename }) => filename,
});

export const action: ActionFunction = async ({ request }) => {
const formData = await unstable_parseMultipartFormData(request, fileUploadHandler);
console.log(formData.get('upload')); // will return the filename

return {};
};

const Upload = () => {
return (
<Form method="post" encType="multipart/form-data">
<input type="file" name="upload" />
<button type="submit">upload</button>
</Form>
);
};

export default Upload;

The documentation also has an example to stream your upload to Cloudinary, with a custom uploadHandler.

But, since I'm using the Google Cloud Platform, I want my files to be stored in a Cloud Storage bucket.

Therefor, I used the @google-cloud/storage package.

My route now looks like this

// ./app/routes/upload.tsx

import { ActionFunction, Form, unstable_parseMultipartFormData, useActionData } from 'remix';
import { cloudStorageUploaderHandler } from '~/services/upload-handler.server';

export const action: ActionFunction = async ({ request }) => {
const formData = await unstable_parseMultipartFormData(request, cloudStorageUploaderHandler);
const filename = formData.get('upload');

return { filename };
};

const Upload = () => {
const actionData = useActionData();

if (actionData && actionData.filename) {
return <>Upload successful.</>;
}

return (
<Form method="post" encType="multipart/form-data">
<input type="file" name="upload" />
<button type="submit">upload</button>
</Form>
);
};

export default Upload;

I created a service that should only run server-side in ./app/services/uploader-handler.server.ts which looks like this.

import { Readable } from 'stream';
import { Storage } from '@google-cloud/storage';
import { UploadHandler } from 'remix';

const uploadStreamToCloudStorage = async (fileStream: Readable, fileName: string) => {
const bucketName = 'YOUR_BUCKET_NAME';

// Create Cloud Storage client
const cloudStorage = new Storage();

// Create a reference to the file.
const file = cloudStorage.bucket(bucketName).file(fileName);

async function streamFileUpload() {
fileStream.pipe(file.createWriteStream()).on('finish', () => {
// The file upload is complete
console.log('File upload complete');
});

console.log(`${fileName} uploaded to ${bucketName}`);
}

streamFileUpload().catch(console.error);

return fileName;
};

export const cloudStorageUploaderHandler: UploadHandler = async ({
filename,
stream: fileStream,
}) => {
return await uploadStreamToCloudStorage(fileStream, filename);
};

Et voila, the file from the form, is now directly streamed to Google Cloud Storage.