Remix File Upload
Upload Files from Remix Action Functions with fetch
Upload files from Remix action functions with fetch. No SDK, REST API, CDN delivery.
In Remix, the action function is where mutations live, and it runs only on the server — which makes it the natural home for a file upload that must keep its API key secret. files.link needs three fetch calls there: POST metadata, PUT bytes, POST confirm. No aws-sdk, no platform-specific storage binding, and the same action code runs whether you deploy to a Node server, Vercel, or Cloudflare Pages.
The Remix-specific wrinkle is multipart parsing. A browser form with enctype='multipart/form-data' is parsed with unstable_parseMultipartFormData (or the newer parseFormData helpers), using a memory or upload handler to get the file out. Once you have the File, you read its bytes and PUT them.
import { unstable_parseMultipartFormData, unstable_createMemoryUploadHandler } from '@remix-run/node';
export async function action({ request }: ActionFunctionArgs) {
const form = await unstable_parseMultipartFormData(
request, unstable_createMemoryUploadHandler());
const file = form.get('upload') as File;
const key = process.env.FILESLINK_KEY!;
const r1 = await fetch(`https://api.files.link/v1/files/${process.env.FOLDER_ID}`, {
method: 'POST',
headers: { Authorization: key, 'Content-Type': 'application/json' },
body: JSON.stringify({ filesMetadata: [{ name: file.name, size: file.size, type: file.type }] }),
});
const slot = (await r1.json()).urls[0];
await fetch(slot.url, { method: 'PUT', headers: { 'Content-Type': file.type || 'application/octet-stream' }, body: await file.arrayBuffer() });
await fetch('https://api.files.link/v1/files/confirm-upload', {
method: 'POST',
headers: { Authorization: key, 'Content-Type': 'application/json' },
body: JSON.stringify({ ids: [slot.id] }),
});
return json({ id: slot.id });
}Return the new file id (or CDN URL) from the action and Remix revalidates your loaders automatically, so the UI updates without a manual refetch. On Cloudflare Pages, the only thing to watch is that the multipart helper you import is the one compatible with the Workers runtime — fetch itself is native there. For large files, prefer streaming the upload to the presigned URL or doing a direct browser-to-storage PUT and only calling confirm-upload from the action, which keeps you under platform request limits.
The API key stays in process.env, never in a loader that ships data to the client. Files return as CDN URLs with per-file visibility, encrypted at rest, billed from prepaid credits.
Benefits With No Complexity
Global CDN delivery
Edge-cached worldwide
Signed-URL security
What You Get
Unlimited files
Unlimited storage
Public + Private storage
CDN ready links
Prepaid credits
More coming soon
How files.link Works
1. Upload
2. Copy
3. Use Anywhere
Why Developers Choose files.link
A Better Way to Store & Deliver Files
| AWS S3 (aws-sdk) | Cloudflare R2 binding | files.link | |
|---|---|---|---|
| Works in action() (no SDK install) | |||
| Same code on Node / Vercel / CF Pages | |||
| Auto loader revalidation after upload | |||
| Global CDN delivery on every file | |||
| Prepaid credits (no surprise bills) |
Calculate Your Needs
Storage
Egress
CDN Bandwidth
Wire Up Remix File Uploads
- Global CDN delivery
- Edge-cached worldwide
- Signed-URL security
- Unlimited files
- Unlimited storage
- Public + Private storage
- No subscription — prepaid credits keep spend predictable