This doc is a WIP: It was extracted from the API docs for file uploads so it’s a bit out of context. We intend to re-write this as a general guide on file uploads.

Most of the time, you’ll probably want to proxy the file to a file host.

Example:

  1. import type {
  2. ActionArgs,
  3. UploadHandler,
  4. } from "@remix-run/node"; // or cloudflare/deno
  5. import {
  6. unstable_composeUploadHandlers,
  7. unstable_createMemoryUploadHandler,
  8. unstable_parseMultipartFormData,
  9. } from "@remix-run/node"; // or cloudflare/deno
  10. import { writeAsyncIterableToWritable } from "@remix-run/node"; // `writeAsyncIterableToWritable` is a Node-only utility
  11. import type {
  12. UploadApiOptions,
  13. UploadApiResponse,
  14. UploadStream,
  15. } from "cloudinary";
  16. import cloudinary from "cloudinary";
  17. async function uploadImageToCloudinary(
  18. data: AsyncIterable<Uint8Array>
  19. ) {
  20. const uploadPromise = new Promise<UploadApiResponse>(
  21. async (resolve, reject) => {
  22. const uploadStream =
  23. cloudinary.v2.uploader.upload_stream(
  24. {
  25. folder: "remix",
  26. },
  27. (error, result) => {
  28. if (error) {
  29. reject(error);
  30. return;
  31. }
  32. resolve(result);
  33. }
  34. );
  35. await writeAsyncIterableToWritable(
  36. data,
  37. uploadStream
  38. );
  39. }
  40. );
  41. return uploadPromise;
  42. }
  43. export const action = async ({ request }: ActionArgs) => {
  44. const userId = getUserId(request);
  45. const uploadHandler = unstable_composeUploadHandlers(
  46. // our custom upload handler
  47. async ({ name, contentType, data, filename }) => {
  48. if (name !== "img") {
  49. return undefined;
  50. }
  51. const uploadedImage = await uploadImageToCloudinary(
  52. data
  53. );
  54. return uploadedImage.secure_url;
  55. },
  56. // fallback to memory for everything else
  57. unstable_createMemoryUploadHandler()
  58. );
  59. const formData = await unstable_parseMultipartFormData(
  60. request,
  61. uploadHandler
  62. );
  63. const imageUrl = formData.get("avatar");
  64. // because our uploadHandler returns a string, that's what the imageUrl will be.
  65. // ... etc
  66. };

The UploadHandler function accepts a number of parameters about the file:

PropertyTypeDescription
namestringThe field name (comes from your HTML form field “name” value)
dataAsyncIterableThe iterable of the file bytes
filenamestringThe name of the file that the user selected for upload (like rickroll.mp4)
contentTypestringThe content type of the file (like videomp4)

Your job is to do whatever you need with the data and return a value that’s a valid [FormData][form-data] value: [File][the-browser-file-api], string, or undefined to skip adding it to the resulting FormData.

Upload Handler Composition

We have the built-in unstable_createFileUploadHandler and unstable_createMemoryUploadHandler and we also expect more upload handler utilities to be developed in the future. If you have a form that needs to use different upload handlers, you can compose them together with a custom handler, here’s a theoretical example:

  1. import type { UploadHandler } from "@remix-run/node"; // or cloudflare/deno
  2. import { unstable_createFileUploadHandler } from "@remix-run/node"; // or cloudflare/deno
  3. import { createCloudinaryUploadHandler } from "some-handy-remix-util";
  4. export const standardFileUploadHandler =
  5. unstable_createFileUploadHandler({
  6. directory: "public/calendar-events",
  7. });
  8. export const cloudinaryUploadHandler =
  9. createCloudinaryUploadHandler({
  10. folder: "/my-site/avatars",
  11. });
  12. export const fileUploadHandler: UploadHandler = (args) => {
  13. if (args.name === "calendarEvent") {
  14. return standardFileUploadHandler(args);
  15. } else if (args.name === "eventBanner") {
  16. return cloudinaryUploadHandler(args);
  17. }
  18. return undefined;
  19. };