How to download an image using Node.js

By

Learn how to download an image or file in Node.js with the built-in fetch API and fs, piping the response into createWriteStream to save it to disk.

~~~

To download an image in Node.js, request it with the built-in fetch API (Node 18+) and write the body to disk with fs.createWriteStream(). No extra HTTP library needed.

I asked myself this question when I had to download a file from a server, programmatically.

I had to connect to a server, download a file, and store it locally.

This is the code, updated to use the built-in fetch API:

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

const download = async (url, path) => {
  const response = await fetch(url)

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

  await pipeline(
    Readable.fromWeb(response.body),
    fs.createWriteStream(path)
  )
}

const url = 'https://flaviocopes.com/img/avatar.png'
const path = './images/avatar.png'

await download(url, path)
console.log('✅ Done!')

fetch ships with Node.js since version 18 (stable since Node 21), and Readable.fromWeb() was marked stable in Node 24. You do not install anything for this.

The node: prefix on the imports marks fs and stream as built-in Node modules, so nobody mistakes them for npm packages. It’s the form the Node docs use today.

If the file is small and you prefer a buffer, this also works:

const response = await fetch(url)
const buffer = Buffer.from(await response.arrayBuffer())
fs.writeFileSync(path, buffer)

The stream + pipeline version is better for larger files. The whole image is never held in memory at once.

How does it work?

fetch(url) returns a Response. response.body is a Web ReadableStream.

Readable.fromWeb() turns that into a Node.js readable stream. fs.createWriteStream(path) creates a writable stream pointing at a file on disk.

pipeline() connects the two and forwards errors for you. Chunks of the image are written to the file as they come in over the network.

You can inspect headers before writing if you need to:

const response = await fetch(url)
console.log(response.headers.get('content-type'))
console.log(response.headers.get('content-length'))

That is the same idea as the old request.head() call, without a separate package.

A pitfall: the destination folder must exist

fs.createWriteStream() creates the file, but not the folders in the path.

If the ./images folder does not exist, the download fails with an ENOENT error. Create it first:

fs.mkdirSync('./images', { recursive: true })

The recursive option makes it a no-op when the folder is already there, so it’s safe to call every time.

Also consider handling errors. pipeline() rejects on a dropped connection, so wrap the call in try/catch if you need the download to fail gracefully.

What about the old request package?

Older tutorials (including an earlier version of this post) used the request module and piped it into createWriteStream(). That package was deprecated in February 2020 and gets no updates. Use the built-in fetch instead. If you need lower-level control, how to download an image from URL in Node shows the same download with the built-in node:https module, redirects included.

If you want a third-party HTTP client instead, Axios works fine for downloads too. And if you are sending data the other way, see how to make an HTTP POST request in Node.js.

Tagged: Node.js · All topics

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

~~~

Related posts about node: