How to download an image from URL in Node

By

Learn how to download an image from a URL in Node.js using native fetch and a stream pipeline, with an https.get alternative for redirects and low-level control.

~~~

To download an image from a URL in Node.js, use native fetch and pipe the response body into a file. You don’t need any third-party package. fetch has shipped without flags since Node 18, and Readable.fromWeb() (used below) became stable in Node 24, the current LTS.

This is the full code:

import { createWriteStream } from 'node:fs'
import { pipeline } from 'node:stream/promises'
import { Readable } from 'node:stream'

async function downloadImage(url, filePath) {
  const response = await fetch(url)

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`)
  }

  if (!response.body) {
    throw new Error('Response has no body')
  }

  await pipeline(
    Readable.fromWeb(response.body),
    createWriteStream(filePath)
  )

  return filePath
}

await downloadImage('https://flaviocopes.com/img/og.png', './og.png')

fetch follows redirects for you. Readable.fromWeb() turns the Web stream from response.body into a Node stream so pipeline can write it to disk.

Why streams?

An image can easily be a few megabytes. The pipeline writes each chunk to disk as it arrives from the network, so the whole file never sits in memory.

That’s a big deal when you download many images in parallel, like in a scraper or a build script that fetches remote assets.

An https.get alternative

If you need low-level control, or you’re on a very old Node without fetch, the built-in https module still works. You handle redirects yourself:

import { createWriteStream } from 'node:fs'
import https from 'node:https'

function downloadImage(url, filePath) {
  return new Promise((resolve, reject) => {
    https
      .get(url, (response) => {
        const code = response.statusCode ?? 0

        if (code >= 400) {
          return reject(new Error(response.statusMessage))
        }

        //handle redirects
        if (code > 300 && code < 400 && response.headers.location) {
          return resolve(downloadImage(response.headers.location, filePath))
        }

        //save the file to disk
        const fileWriter = createWriteStream(filePath)

        fileWriter.on('finish', () => resolve(filePath))
        fileWriter.on('error', reject)

        response.pipe(fileWriter)
      })
      .on('error', reject)
  })
}

await downloadImage('https://flaviocopes.com/img/og.png', './og.png')

Many image URLs don’t point directly at the file. A CDN might answer with a 301 or 302 and a Location header. https.get() does not follow redirects for you, so the middle check calls downloadImage() again with the new URL.

We also reject on any status code of 400 or higher. Without that check, a missing image would save the 404 error page to disk with a .png name, and you’d only find out later when the file won’t open.

A common pitfall

With the https version, listen for finish on the write stream, not for end on the response. The response can end while the last chunk is still being flushed to disk. Resolve too early and you might read a truncated image.

Also attach an error handler to the write stream. Without it, a disk problem (a folder that doesn’t exist, missing permissions) crashes the process instead of rejecting the promise. pipeline handles that for you in the fetch version.

Tagged: Node.js · All topics

Want me to talk about your product? You can sponsor this site.

~~~

Related posts about node: