# File upload through Forms

URL: https://developers.uidu.org/docs/recipes/file-upload

> Use the uploadPresignedUrl flow to attach files to a form submission.

`<DynamicForm>` has no file input — an `attachments` question renders as a plain text input. To
collect files, render that question yourself with the presigned-upload flow below.

## How it works

1. **Read the upload target from the form.** `getForm` returns it on each media question's field:
   `field.uploadPresignedUrl` is the presign as JSON — `{ url, fields }`, a presigned **POST** — and
   `field.uploadUrl` / `field.uploadParams` are its two halves. This is a public read.
2. **POST the file** from the browser straight to `uploadUrl`, as multipart form data: every entry of
   `uploadParams`, then the file. No uidu token is involved.
3. **Submit the response** from a server action, with the uploaded file as a linked `Attachment` on
   that question's field value.

<Callout type="warn" title="Read the form close to the upload">
  Every read of the form signs a fresh target, and it expires after a short while. Only media fields
  have one; other fields return it empty.
</Callout>

## The page

```tsx
// app/apply/page.tsx — a Server Component
import { getForm } from '@uidu/client';
import { notFound } from 'next/navigation';
import { uidu } from '@/lib/uidu';
import { ApplyForm } from './apply-form';

const FORM_ID = 'Z2lkOi8v…';

export default async function ApplyPage() {
  const form = await getForm(uidu, { id: FORM_ID });
  if (!form) notFound();

  const fileQuestion = form.inputs.edges
    ?.map((e) => e?.node)
    .find((q) => q?.field.kind === 'attachments');
  if (!fileQuestion) notFound();

  return (
    <ApplyForm
      formId={form.id}
      fieldId={fileQuestion.field.id}
      uploadUrl={fileQuestion.field.uploadUrl as string}
      uploadParams={(fileQuestion.field.uploadParams ?? {}) as Record<string, string>}
    />
  );
}
```

## The upload (client)

```tsx
// app/apply/apply-form.tsx
'use client';

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

export function ApplyForm(props: {
  formId: string;
  fieldId: string;
  uploadUrl: string;
  uploadParams: Record<string, string>;
}) {
  const [file, setFile] = useState<string | null>(null);
  const [status, setStatus] = useState<'idle' | 'uploading' | 'uploaded' | 'sent'>('idle');

  async function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    const picked = e.target.files?.[0];
    if (!picked) return;
    setStatus('uploading');

    const body = new FormData();
    Object.entries(props.uploadParams).forEach(([key, value]) => body.append(key, value));
    body.append('file', picked); // the file goes last, after the signed fields

    const res = await fetch(props.uploadUrl, { method: 'POST', body });
    if (!res.ok) throw new Error('upload failed');

    // uidu's attachment data for the uploaded file: the object key without its `cache/` prefix.
    setFile(
      JSON.stringify({
        id: props.uploadParams.key.replace(/^cache\//, ''),
        storage: 'cache',
        metadata: { filename: picked.name, mime_type: picked.type, size: picked.size },
      }),
    );
    setStatus('uploaded');
  }

  async function send() {
    if (!file) return;
    const result = await submitWithFile(props.formId, props.fieldId, file);
    if (result.ok) setStatus('sent');
  }

  return (
    <div>
      <input type="file" onChange={handleChange} />
      <button disabled={status !== 'uploaded'} onClick={send}>
        Send
      </button>
      <span>{status}</span>
    </div>
  );
}
```

## Submitting (server action)

The response is a write, so it runs on the server with the API key client:

```ts
// app/apply/actions.ts
'use server';

import { createFormResponse } from '@uidu/client';
import { uidu } from '@/lib/uidu'; // createClient({ workspace, publicToken, apiKey })

export async function submitWithFile(formId: string, fieldId: string, file: string) {
  const result = await createFormResponse(uidu, {
    input: {
      attributes: {
        formId,
        fieldValuesAttributes: [
          {
            fieldId,
            linkedRecordType: 'Attachment',
            linkedRecordAttributes: { file }, // the attachment data from the upload
          },
          // other answers as usual: { fieldId, content: { value } }
        ],
      },
      event: 'complete!',
    },
  });

  if (result?.errors?.length) return { ok: false as const, errors: result.errors };
  return { ok: true as const, responseId: result?.formResponse?.id };
}
```

* `'Attachment'` is the only `linkedRecordType` the API accepts on a field value; the file lives in
  that linked record, not in `content`.
* Every other answer in the same response still uses `content: { value }` — see
  [Forms](https://developers.uidu.org/docs/domains/forms.md).
* The same flow serves a call's application form: its questions are at
  `call.form.inputs.edges[].node` — see [Calls](https://developers.uidu.org/docs/domains/calls.md).

More on the response shape, drafts and statuses: [Forms](https://developers.uidu.org/docs/domains/forms.md).
