---
title: File Uploads
---
# File Uploads
[MODES: framework]
_Thank you to David Adams for [writing an original guide](https://programmingarehard.com/2024/09/06/remix-file-uploads-updated.html/) on which this doc is based. You can refer to it for even more examples._
## Basic File Upload
### 1. Setup some routes
You can setup your routes however you like. This example uses the following structure:
```ts filename=routes.ts
import {
type RouteConfig,
route,
} from "@react-router/dev/routes";
export default [
// ... other routes
route("user/:id", "pages/user-profile.tsx", [
route("avatar", "api/avatar.tsx"),
]),
] satisfies RouteConfig;
```
### 2. Add the form data parser
`form-data-parser` is a wrapper around `request.formData()` that provides streaming support for handling file uploads.
```shellscript
npm i @remix-run/form-data-parser
```
[See the `form-data-parser` docs for more information][form-data-parser]
### 3. Create a route with an upload action
The `parseFormData` function takes an `uploadHandler` function as an argument. This function will be called for each file upload in the form.
You must set the form's `enctype` to `multipart/form-data` for file uploads to work.
```tsx filename=pages/user-profile.tsx
import {
type FileUpload,
parseFormData,
} from "@remix-run/form-data-parser";
import type { Route } from "./+types/user-profile";
export async function action({
request,
}: Route.ActionArgs) {
const uploadHandler = async (fileUpload: FileUpload) => {
if (fileUpload.fieldName === "avatar") {
// process the upload and return a File
}
};
const formData = await parseFormData(
request,
uploadHandler,
);
// 'avatar' has already been processed at this point
const file = formData.get("avatar");
}
export default function Component() {
return (
);
}
```
## Local Storage Implementation
### 1. Add the storage package
`file-storage` is a key/value interface for storing [File objects][file] in JavaScript. Similar to how `localStorage` allows you to store key/value pairs of strings in the browser, file-storage allows you to store key/value pairs of files on the server.
```shellscript
npm i @remix-run/file-storage
```
[See the `file-storage` docs for more information][file-storage]
### 2. Create a storage configuration
Create a file that exports a `LocalFileStorage` instance to be used by different routes.
```ts filename=avatar-storage.server.ts
import { LocalFileStorage } from "@remix-run/file-storage/local";
export const fileStorage = new LocalFileStorage(
"./uploads/avatars",
);
export function getStorageKey(userId: string) {
return `user-${userId}-avatar`;
}
```
### 3. Implement the upload handler
Update the form's `action` to store files in the `fileStorage` instance.
```tsx filename=pages/user-profile.tsx
import {
type FileUpload,
parseFormData,
} from "@remix-run/form-data-parser";
import {
fileStorage,
getStorageKey,
} from "~/avatar-storage.server";
import type { Route } from "./+types/user-profile";
export async function action({
request,
params,
}: Route.ActionArgs) {
async function uploadHandler(fileUpload: FileUpload) {
if (
fileUpload.fieldName === "avatar" &&
fileUpload.type.startsWith("image/")
) {
let storageKey = getStorageKey(params.id);
// FileUpload objects are not meant to stick around for very long (they are
// streaming data from the request.body); store them as soon as possible.
await fileStorage.set(storageKey, fileUpload);
// Return a File for the FormData object. This is a LazyFile that knows how
// to access the file's content if needed (using e.g. file.stream()) but
// waits until it is requested to actually read anything.
return fileStorage.get(storageKey);
}
}
const formData = await parseFormData(
request,
uploadHandler,
);
}
export default function UserPage({
actionData,
params,
}: Route.ComponentProps) {
return (
User {params.id}
);
}
```
### 4. Add a route to serve the uploaded file
Create a [resource route][resource-route] that streams the file as a response.
```tsx filename=api/avatar.tsx
import {
fileStorage,
getStorageKey,
} from "~/avatar-storage.server";
import type { Route } from "./+types/avatar";
export async function loader({ params }: Route.LoaderArgs) {
const storageKey = getStorageKey(params.id);
const file = await fileStorage.get(storageKey);
if (!file) {
throw new Response("User avatar not found", {
status: 404,
});
}
return new Response(file.stream(), {
headers: {
"Content-Type": file.type,
"Content-Disposition": `attachment; filename=${file.name}`,
},
});
}
```
[form-data-parser]: https://www.npmjs.com/package/@remix-run/form-data-parser
[file-storage]: https://www.npmjs.com/package/@remix-run/file-storage
[file]: https://developer.mozilla.org/en-US/docs/Web/API/File
[resource-route]: ../how-to/resource-routes