How to download and save an image using Node.js
By Flavio Copes
Learn how to download and save an image in Node.js with native fetch and stream.pipeline, plus an Axios stream alternative if you already use Axios.
To download and save an image in Node.js, fetch it and pipe the response body into a file. On Node 20 or newer (the current LTS is Node 24) you don’t need a library for this: global fetch is enough. I had the need of downloading a file from the Internet, and I also wanted to use await to do something else after in an easy way. This is the pattern I use.
Why a stream? An image can be big. If you buffer the whole response in memory first, you pay for that RAM. With a stream, the data flows from the network straight into the file, a chunk at a time. This is what Node.js streams are for.
Start with the imports:
import { createWriteStream } from 'node:fs'
import { pipeline } from 'node:stream/promises'
Then write a download() function like this:
async function download(url, filepath) {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`download failed: ${response.status} ${response.statusText}`)
}
await pipeline(response.body, createWriteStream(filepath))
return filepath
}
fetch() returns a Response. response.body is a web ReadableStream, and pipeline() from node:stream/promises accepts it directly. pipeline() waits until the write finishes, and it cleans up both sides if something fails.
Because the function returns a promise, you can await it:
const url = 'https://flaviocopes.com/img/og.png'
await download(url, './images/og.png')
console.log('saved!')
The console.log only runs after the image is saved. That was the whole point for me: do the next thing only when the download is complete.
Using Axios instead
If your project already depends on Axios, the same idea works with responseType: 'stream':
import fs from 'node:fs'
import axios from 'axios'
async function download(url, filepath) {
const response = await axios({
url,
method: 'GET',
responseType: 'stream',
})
return new Promise((resolve, reject) => {
response.data
.pipe(fs.createWriteStream(filepath))
.on('error', reject)
.once('close', () => resolve(filepath))
})
}
For new scripts with no HTTP client yet, prefer native fetch. Reach for Axios when you already use it for the rest of the app.
Two things that can go wrong
First, the destination folder must exist. createWriteStream('./images/og.png') fails with an ENOENT error if there’s no images folder. Create it first:
import { mkdirSync } from 'node:fs'
mkdirSync('./images', { recursive: true })
The recursive option also makes this a no-op when the folder already exists.
Second, check the HTTP status. fetch() does not throw on a 404. That’s why the native example checks response.ok before writing. Wrap the call in a try/catch so a broken URL doesn’t crash your script:
try {
await download(url, './images/og.png')
} catch (error) {
console.error('download failed:', error.message)
}
This same function works for any file, not just images. PDFs, zip archives, videos: the stream doesn’t care what the bytes are. The Fetch API post covers more of what fetch() can do beyond a simple GET.
Want me to talk about your product? You can sponsor this site.
Related posts about node: