# File uploads

> Sending files through a server action without encoding them.

A `File` is a value a server action can take. React's Flight format carries it
as binary, so there is nothing to encode on the way out and nothing to decode
on the way in.

## One file

```tsx title="src/components/AvatarUpload.tsx"
"use client";

import { useState } from 'react';
import { uploadAvatar } from '../actions';

export function AvatarUpload() {
  const [url, setUrl] = useState<string | null>(null);

  async function submit(formData: FormData) {
    const file = formData.get('avatar') as File;

    if (!file || file.size === 0) return;

    setUrl(await uploadAvatar(file));
  }

  return (
    <form action={submit}>
      <input type="file" name="avatar" accept="image/*" />
      <button type="submit">Upload</button>
      {url && <img src={url} alt="" />}
    </form>
  );
}
```

The action receives the `File` itself — name, type, size and all:

```ts title="src/actions.ts"
'use server'

import { writeFile } from 'node:fs/promises';

export async function uploadAvatar(file: File): Promise<string> {
  const bytes = new Uint8Array(await file.arrayBuffer());

  await writeFile(`./public/avatars/${file.name}`, bytes);

  return `/avatars/${file.name}`;
}
```

Nothing about that is upload-specific: it is the same call, the same
serialisation and the same round trip as [any other server
action](/guides/server-actions).

## The whole form at once

An action can take the `FormData` instead of individual arguments, which is
usually simpler when the form mixes files and fields:

```tsx
"use client";

import { createPost } from '../actions';

export function NewPost() {
  return (
    <form action={createPost}>
      <input name="title" />
      <textarea name="body" />
      <input type="file" name="cover" accept="image/*" />
      <button type="submit">Create</button>
    </form>
  );
}
```

```ts title="src/actions.ts"
'use server'

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const cover = formData.get('cover') as File;

  // …
}
```

## Several files

`formData.getAll` gives every file from a `multiple` input, and an array of
`File` serialises as readily as one:

```tsx
const files = formData.getAll('photos') as File[];

await uploadPhotos(files);
```

```ts title="src/actions.ts"
'use server'

export async function uploadPhotos(files: File[]) {
  for (const file of files) {
    // …
  }
}
```

## Size

The whole body is buffered before the action runs — it is one `POST`, not a
stream — so an upload occupies memory for as long as it takes. The server
refuses a body over **8 MB** with a 413 before it holds a byte of it, and
the form reports that as a failed submission. Raise the ceiling when the app
genuinely uploads more:

```ts title="vite.config.ts"
rscKit({ maxActionBody: 32 * 1024 * 1024 })
```

Two consequences worth planning for:

- Keep the ceiling — and any limit a proxy in front imposes — as low as the app actually needs.
- For genuinely large files, do not send them through an action at all. Have the action mint a pre-signed URL and let the browser upload straight to storage; the bytes never touch your server.
