🔗 files.link
tutorials

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. Keep FILESLINK_KEY server-side; never expose it to the client bundle.
  • Express / Fastify: same code from your route handler. Use busboy or formidable to 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.