How to Download a File with Node.js Without Third-Party Libraries

How to Download a File with Node.js Without Third-Party Libraries

Downloading files in Node.js is a fundamental task that used to require heavy external dependencies like Axios or Request. However, with the stabilization of the Fetch API in modern Node.js versions (v18+) and the long-standing power of the https and fs modules, you can build a robust downloader using only native tools.

This guide covers the two primary ways to download files without third-party libraries: using the modern Fetch API for promise-based simplicity and the legacy https module for maximum control over streams.

Method 1: Using the Native Fetch API (Node.js 18+)

The Fetch API is now stable and included in the global scope of Node.js. It is the most modern, readable, and efficient way to handle HTTP requests. Because the response body of a fetch request is a web standard ReadableStream, we use Readable.fromWeb to convert it into a Node-compatible stream for the filesystem.

const fs = require('fs');
const { Readable } = require('stream');
const { finished } = require('stream/promises');
const path = require('path');

async function downloadWithFetch(url, dest) {
const response = await fetch(url);

if (!response.ok) {
    throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
}

const fileStream = fs.createWriteStream(dest);

// Convert web stream to Node stream and pipe to file
await finished(Readable.fromWeb(response.body).pipe(fileStream));

console.log('Download complete via Fetch');
}

// Usage
downloadWithFetch('https://example.com/image.jpg', './image.jpg')
.catch(err => console.error(err));

Method 2: Using the Built-in HTTPS Module

If you are working on a legacy environment (Node.js 16 or older) or need to handle complex redirect logic manually, the https module is the standard choice. It utilizes a callback-based approach and pipes the incoming data directly to a write stream.

const https = require('https');
const fs = require('fs');

function downloadWithHttps(url, dest) {
const file = fs.createWriteStream(dest);

https.get(url, (response) => {
    // Handle Redirects (Status 301, 302)
    if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
        return downloadWithHttps(response.headers.location, dest);
    }

    if (response.statusCode !== 200) {
        console.error(`Request Failed. Status Code: ${response.statusCode}`);
        return;
    }

    response.pipe(file);

    file.on('finish', () => {
        file.close();
        console.log('Download complete via HTTPS module');
    });
}).on('error', (err) => {
    fs.unlink(dest, () => {}); // Delete the partial file on error
    console.error(`Error: ${err.message}`);
});
}

// Usage
downloadWithHttps('https://example.com/data.zip', './data.zip');

Key Considerations for Production

When building a file downloader for a production environment, simply “piping” the data is often not enough. You must account for edge cases that can lead to corrupted files or memory leaks.

1. Handling Redirects

Unlike third-party libraries like Axios, the native https.get method does not automatically follow redirects. If the server responds with a 301 or 302 status code, you must recursively call your download function using the location header. The Fetch API (Method 1) handles this automatically by default.

2. Proper Error Handling and Cleanup

If a network connection drops mid-download, the fs.createWriteStream will often leave a partial, corrupted file on your disk. It is best practice to listen for the error event on the request and use fs.unlink() to delete the incomplete file.

3. Monitoring Download Progress

To implement a progress bar without libraries, you can listen to the data event on the response stream and compare the received chunks against the content-length header:

let downloadedBytes = 0;
const totalBytes = parseInt(response.headers['content-length'], 10);

response.on('data', (chunk) => {
downloadedBytes += chunk.length;
const progress = ((downloadedBytes / totalBytes) * 100).toFixed(2);
process.stdout.write(Progress: ${progress}%\r);
});

Summary of Best Practices

  • Use Fetch for Modern Apps: If you are on Node.js 18 or higher, use the Fetch API. It is cleaner, promise-based, and handles redirects automatically.
  • Prefer Streams over Buffers: Never use fs.writeFile() for downloads. Streams (pipe) ensure that you don’t load the entire file into RAM, which is critical for large files.
  • Check Status Codes: Always verify that the response status is 200 before writing to the disk to avoid saving HTML error pages as your “file.”
  • Close Streams: Ensure file.close() is called in the finish event to flush all data to the disk and release the file descriptor.

Frequently Asked Questions

Can I download files over HTTP instead of HTTPS?

Yes, simply replace require('https') with require('http'). However, most modern web servers enforce SSL, so the https module is usually the safer default.

Is the native Fetch API as fast as Axios?

Yes. Native Fetch in Node.js is built on Undici, a high-performance HTTP/1.1 client that is often faster and more memory-efficient than older libraries like Axios or Request.

Why is my file empty after download?

This usually happens if the server sent a redirect (301/302) and you didn’t follow it, or if the write stream was closed before the data finished piping. Ensure you are handling the finish event correctly.

Conclusion

By leveraging the native Fetch API or the standard https module, you can significantly reduce the weight of your Node.js projects. Eliminating third-party dependencies reduces your security attack surface and simplifies maintenance. Whether you choose the modern promise-based approach of Fetch or the granular control of streams via https, Node.js provides all the tools necessary for efficient, professional-grade file management.

Al Mahbub Khan
Written by Al Mahbub Khan Full-Stack Developer & Adobe Certified Magento Developer