How to Upload Files from Node.js with Built-In fetch
Step-by-step guide to uploading files from Node.js via the files.link REST API — using the built-in fetch (Node 18+), no aws-sdk required.
Why this matters in 2026
Node 18 made fetch global. Node 20 made it stable. That means any modern Node.js project can talk to HTTP APIs without installing axios, node-fetch, or any third-party HTTP client. For files.link uploads, that means zero new dependencies in your package.json.
If you're still installing aws-sdk or @aws-sdk/client-s3 just to upload a file from a Node.js service, you're shipping 30+ MB of vendored code (including all of aws-sdk's request signing, retry middleware, and protocol abstractions) when fetch + 20 lines would do it.
The 3-step presigned flow
files.link uses presigned URLs so the actual file bytes go directly to object storage, never through our backend. Three HTTP calls total:
1. POST file metadata to /v1/files/{folderId} → response includes a presigned PUT URL.
2. PUT file bytes to the presigned URL.
3. POST to /v1/files/confirm-upload with the file id → row becomes visible.
Code: minimal Node.js upload
Here's the whole thing in ~25 lines:
import { readFile, stat } from "node:fs/promises";
import { basename } from "node:path";
const API_KEY = process.env.FILESLINK_KEY;
const FOLDER_ID = "your-folder-uuid-here";
const BASE = "https://api.files.link";
async function upload(path) {
const filename = basename(path);
const { size } = await stat(path);
// Step 1: ask for a presigned URL
const metaRes = await fetch(`${BASE}/v1/files/${FOLDER_ID}`, {
method: "POST",
headers: {
"Authorization": API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ filesMetadata: [{ name: filename, size }] }),
});
if (!metaRes.ok) throw new Error(`Metadata POST failed: ${metaRes.status}`);
const { urls: [entry] } = await metaRes.json();
// Step 2: PUT the file bytes
const bytes = await readFile(path);
const putRes = await fetch(entry.url, { method: "PUT", body: bytes });
if (!putRes.ok) throw new Error(`PUT failed: ${putRes.status}`);
// Step 3: confirm
const confirmRes = await fetch(`${BASE}/v1/files/confirm-upload`, {
method: "POST",
headers: {
"Authorization": API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ ids: [entry.id] }),
});
if (!confirmRes.ok) throw new Error(`Confirm failed: ${confirmRes.status}`);
return entry.s3Key; // combine with your CDN base URL to get a public link
}Set FILESLINK_KEY in your environment, point FOLDER_ID at a folder, and you're uploading.
Streaming large files
readFile() loads the whole file into memory. For multi-GB uploads, stream the body instead:
import { createReadStream } from "node:fs";
// In step 2:
const stream = createReadStream(path);
const putRes = await fetch(entry.url, {
method: "PUT",
body: stream,
duplex: "half", // required for ReadableStream bodies
});For files >100MB, use the multipart endpoint (/v1/files/multipart/{folderId}/initiate). You'll get an array of presigned PUT URLs, one per part. PUT them in parallel with Promise.all for the fastest transfer.
Framework integration
The same three calls work from any framework:
- Next.js: drop the upload logic into a route handler (
app/api/upload/route.ts) or a server action. KeepFILESLINK_KEYserver-side; never expose it to the client bundle. - Express / Fastify: same code from your route handler. Use
busboyorformidableto parse multipart bodies from browser clients, then PUT the parsed file to the presigned URL. - Hono / Bun: fetch is universal — same code works on every runtime.
For Node.js file upload, Next.js file upload, or Express file upload, see the dedicated landing pages for framework-specific patterns.