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 !
I consider myself as an IT Business Artisan. Or Consultant CTO. I'm a self-taught Web Developper, coach and teacher. My main work is helping and guiding digital startups.
more about meBTC
18SY81ejLGFuJ9KMWQu5zPrDGuR5rDiauM
ETH
0x519e0eaa9bc83018bb306880548b79fc0794cd08
XMR
895bSneY4eoZjsr2hN2CAALkUrMExHEV5Pbg8TJb6ejnMLN7js1gLAXQySqbSbfzjWHQpQhQpvFtojbkdZQZmM9qCFz7BXU
2024 © My Dynamic Production SRL All rights Reserved.