Nuxt : How to force download a file from an API endpoint.

profile picture

Nuxt : How to force download a file from an API endpoint.

Sometimes when you build a SPA with an API, the Backend will not allow you to download a file by providing the absolute URL to that file, but will instead send a response containing the file 'blob'.

How can you force download that file once the API call is finished?

We will use VueJS and axios to do that.

If you're using Nuxt, I assume you store all your endpoints in some kind of 'repositories'. Something like this:

// /api/postRepository.js
export default ({ store, app: { $axios } }) => ({

    index () {
        return $axios.$get('/posts')
    },

    show (postId) {
        return $axios.$get(`/posts/${postId}`)
    }

})

And then in your components, you would do something like:

<template>
    <div>
        <h1>{{ post.title }}</h1>
    </div>
</template>

<script>
export default {
    data() {
        return {
            post: {}
        }
    },

    created() {
        this.getPost()
    },

    methods: {
        getPost() {
            this.$postRepository.index()
            .then(response => {
                this.post = response.data
            })
        }
    }
}
</script>

Now imagine your post has an attachment. A file that you want your users to download, but you don't want to give them the URL of that file. You want them to only be able to download the file.

Let's write the HTML first:

<template>
    <div>
        <h1>{{ post.title }}</h1>
        <button @click="downloadPostAttachment(post.id)">Download attachment</button>
    </div>
</template>

<script>
import download from "./mixins/download";

export default {
    mixins: [download],

    data() {
        return {
            post: {}
        }
    },

    created() {
        this.getPost()
    },

    methods: {
        getPost() {
            this.$postRepository.index()
            .then(response => {
                this.post = response.data
            })
        },

        downloadPostAttachment(postId) {
            this.$postRepository.downloadPostAttachment(postId)
            .then(response => {
                this.download(response)
            })
        }
    }
}
</script>

We added a button which the user can click to trigger a new API call from $postRepository.downloadPostAttachment().

And we also call this.download() and we pass the response object as parameter. This method is defined in a reusable mixin, we will see what's inside just below.

// /mixins/download.js
export default {
    methods: {
        download (response) {
            const url = window.URL.createObjectURL(new Blob([response.data], { type: response.data.type }))
            const link = document.createElement('a')
            const contentDisposition = response.headers['content-disposition']

            let fileName = 'unknown'
            if (contentDisposition) {
                let fileNameMatch = contentDisposition.match(/filename="(.+)"/)
                if (!fileNameMatch) {
                    fileNameMatch = contentDisposition.match(/filename=(.+)/)
                    if (fileNameMatch.length === 2) { fileName = fileNameMatch[1] }
                } else if (fileNameMatch.length === 2) { fileName = fileNameMatch[1] }
            }
            link.href = url
            link.setAttribute('download', fileName)
            document.body.appendChild(link)

            link.click()
            link.remove()
            window.URL.revokeObjectURL(url)
        }
    }
}

This helper method will force download the Blob object that is retrieve from the response. It will try to find the filename of the given file. This is provided in the Headers of the HTTP call Response.

We then create a fake <a> element that we click to download the file.

At this point, it's already finished ! Everything should work perfectly.

But we need to check how to make the axios call correctly:

// /api/postRepository.js

import { downloadErrorInterceptor } from './_download'

export default ({ store, app: { $axios } }) => ({

    index () {
        return $axios.$get('/posts')
    },

    show (postId) {
        return $axios.$get(`/posts/${postId}`)
    },
    
  	downloadPostAttachment (postId) {
        $axios.interceptors.response.use(
            (response) => { return response },
            (error) => { return downloadErrorInterceptor(error) }
        )

        return $axios({
            method: 'get',
            url: `/posts/${postId}/attachment`,
            responseType: 'blob'
        })
    }

})

The important part is that you use responseType: 'blob' in the axios configuration.

You also note that I imported another helper, which is an axios interceptor that allows you to correctly handle the error if your BackEnd sends you a JSON error object instead of a Blob response. Very useful to display the error on the front end if anything went wrong.

// /api/_download.js

export const downloadErrorInterceptor = (error) => {
    if (
        error.request.responseType === 'blob' &&
        error.response.data instanceof Blob &&
        error.response.data.type &&
        error.response.data.type.toLowerCase().includes('json')
    ) {
        return new Promise((resolve, reject) => {
            const reader = new FileReader()
            reader.onload = () => {
                error.response.data = JSON.parse(reader.result)
                resolve(Promise.reject(error))
            }

            reader.onerror = () => {
                reject(error)
            }

            reader.readAsText(error.response.data)
        })
    };

    return Promise.reject(error)
}

And that's it !

vuejs
api
nuxt
backend
download
file

2020 My Dynamic Production SPRL All rights Reserved.