Storage ClientVue / Nuxt

Vue / Nuxt

Use the storage client with Vue 3 and Nuxt

Last updated:

Vue / Nuxt

Use @visulima/storage-client with Vue 3 and Nuxt 3 for file uploads with composables and TanStack Vue Query integration.

Installation

Vue 3

npm install @visulima/storage-client @tanstack/vue-query vue

Nuxt 3

npm install @visulima/storage-client @tanstack/vue-query

Setup QueryClient

Vue 3

import { createApp } from "vue";
import { VueQueryPlugin } from "@tanstack/vue-query";
import App from "./App.vue";

const app = createApp(App);
app.use(VueQueryPlugin);
app.mount("#app");

Nuxt 3

Create a plugin file:

// plugins/vue-query.client.ts
import { VueQueryPlugin } from "@tanstack/vue-query";

export default defineNuxtPlugin((nuxtApp) => {
    nuxtApp.vueApp.use(VueQueryPlugin);
});

Basic Upload

Use the useUpload composable for automatic method selection:

<template>
    <div class="app">
        <h1>Storage Client - Vue Example</h1>
        <div class="upload-section">
            <input type="file" @change="handleFileChange" :disabled="isUploading" ref="fileInputRef" />
            <button @click="handleUpload" :disabled="!file || isUploading">
                {{ isUploading ? "Uploading..." : "Upload" }}
            </button>
        </div>
        <div v-if="isUploading" class="progress-section">
            <div>Progress: {{ progress }}%</div>
            <progress :value="progress" max="100" />
        </div>
        <div v-if="error" class="error">Error: {{ error.message }}</div>
        <div v-if="result" class="success">Upload complete! File: {{ result.filename }}</div>
    </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { useUpload } from "@visulima/storage-client/vue";

const file = ref<File | null>(null);
const fileInputRef = ref<HTMLInputElement | null>(null);
const { upload, progress, isUploading, error, result } = useUpload({
    endpointMultipart: "/api/upload/multipart",
    endpointTus: "/api/upload/tus",
    onSuccess: (result) => {
        console.log("Upload successful:", result);
        file.value = null;
        if (fileInputRef.value) {
            fileInputRef.value.value = "";
        }
    },
    onError: (error_) => {
        console.error("Upload error:", error_);
    },
});

const handleFileChange = (e: Event) => {
    const target = e.target as HTMLInputElement;
    const selectedFile = target.files?.[0];
    file.value = selectedFile || null;
};

const handleUpload = async () => {
    if (file.value) {
        try {
            await upload(file.value);
        } catch (error_) {
            console.error("Upload failed:", error_);
        }
    }
};
</script>

Multipart Upload

For traditional form-based uploads:

<script setup lang="ts">
import { useMultipartUpload } from "@visulima/storage-client/vue";

const { upload, progress, isUploading } = useMultipartUpload({
    endpoint: "/api/upload/multipart",
});

const handleFileChange = async (e: Event) => {
    const target = e.target as HTMLInputElement;
    const file = target.files?.[0];
    if (file) {
        await upload(file);
    }
};
</script>

<template>
    <div>
        <input type="file" @change="handleFileChange" />
        <div v-if="isUploading">
            <progress :value="progress" max="100" />
        </div>
    </div>
</template>

TUS Resumable Upload

For large files with pause/resume support:

<script setup lang="ts">
import { useTusUpload } from "@visulima/storage-client/vue";

const { upload, pause, resume, progress, isUploading, isPaused } = useTusUpload({
    endpoint: "/api/upload/tus",
});

const handleFileChange = async (e: Event) => {
    const target = e.target as HTMLInputElement;
    const file = target.files?.[0];
    if (file) {
        await upload(file);
    }
};
</script>

<template>
    <div>
        <input type="file" @change="handleFileChange" />
        <div v-if="isUploading">
            <progress :value="progress" max="100" />
            <button v-if="isPaused" @click="resume">Resume</button>
            <button v-else @click="pause">Pause</button>
        </div>
    </div>
</template>

Batch Upload

Upload multiple files simultaneously:

<script setup lang="ts">
import { useBatchUpload } from "@visulima/storage-client/vue";

const { uploadBatch, progress, isUploading, items, completedCount, errorCount } = useBatchUpload({
    endpoint: "/api/upload/multipart",
    onSuccess: (results) => {
        console.log("Batch upload complete:", results);
    },
});

const handleFilesChange = (e: Event) => {
    const target = e.target as HTMLInputElement;
    const files = Array.from(target.files || []);
    if (files.length > 0) {
        uploadBatch(files);
    }
};
</script>

<template>
    <div>
        <input type="file" multiple @change="handleFilesChange" />
        <div v-if="isUploading">
            <div>Progress: {{ progress }}%</div>
            <div>Completed: {{ completedCount }} / {{ items.length }}</div>
            <div>Errors: {{ errorCount }}</div>
        </div>
    </div>
</template>

File Input Composable

Use the useFileInput composable for drag & drop support:

<script setup lang="ts">
import { ref } from "vue";
import { useFileInput } from "@visulima/storage-client/vue";
import { useBatchUpload } from "@visulima/storage-client/vue";

const { files, inputRef, handleFileChange, handleDrop, handleDragOver, openFileDialog } = useFileInput({
    multiple: true,
});

const { uploadBatch } = useBatchUpload({
    endpoint: "/api/upload/multipart",
});

const handleUpload = () => {
    if (files.value.length > 0) {
        uploadBatch(files.value);
    }
};
</script>

<template>
    <div @drop="handleDrop" @dragover="handleDragOver" style="border: 2px dashed #ccc; padding: 2rem; text-align: center">
        <input ref="inputRef" type="file" multiple @change="handleFileChange" style="display: none" />
        <p>Drag and drop files here or</p>
        <button @click="openFileDialog">Select Files</button>
        <div v-if="files.length > 0">
            <p>Selected: {{ files.length }} files</p>
            <button @click="handleUpload">Upload</button>
        </div>
    </div>
</template>

File Operations

Get File

<script setup lang="ts">
import { ref, watch } from "vue";
import { useGetFile } from "@visulima/storage-client/vue";

const props = defineProps<{ fileId: string }>();

const { data, isLoading, error, meta } = useGetFile({
    endpoint: "/api/files",
    id: () => props.fileId,
});

const url = ref<string | null>(null);

watch(data, (blob) => {
    if (blob) {
        url.value = URL.createObjectURL(blob);
    }
});
</script>

<template>
    <div v-if="isLoading">Loading...</div>
    <div v-else-if="error">Error: {{ error.message }}</div>
    <div v-else-if="data && url">
        <img :src="url" :alt="meta?.filename || 'File'" />
        <p v-if="meta">Filename: {{ meta.filename }}</p>
    </div>
</template>

Delete File

<script setup lang="ts">
import { useDeleteFile } from "@visulima/storage-client/vue";

const props = defineProps<{ fileId: string }>();

const { deleteFile, isLoading } = useDeleteFile({
    endpoint: "/api/files",
});
</script>

<template>
    <button @click="() => deleteFile(props.fileId)" :disabled="isLoading">
        {{ isLoading ? "Deleting..." : "Delete" }}
    </button>
</template>

Get File Metadata

<script setup lang="ts">
import { useGetFileMeta } from "@visulima/storage-client/vue";

const props = defineProps<{ fileId: string }>();

const {
    data: meta,
    isLoading,
    error,
} = useGetFileMeta({
    endpoint: "/api/files",
    id: () => props.fileId,
});
</script>

<template>
    <div v-if="isLoading">Loading...</div>
    <div v-else-if="error">Error: {{ error.message }}</div>
    <div v-else-if="meta">
        <p>Filename: {{ meta.filename }}</p>
        <p>Size: {{ meta.size }} bytes</p>
        <p>Content Type: {{ meta.contentType }}</p>
        <p v-if="meta.url">URL: {{ meta.url }}</p>
    </div>
</template>

Head File (Check Upload Status)

<script setup lang="ts">
import { useHeadFile } from "@visulima/storage-client/vue";

const props = defineProps<{ fileId: string }>();

const { data, isLoading } = useHeadFile({
    endpoint: "/api/files",
    id: () => props.fileId,
});
</script>

<template>
    <div v-if="isLoading">Checking status...</div>
    <div v-else-if="data">
        <p v-if="data.uploadComplete">Upload complete</p>
        <p v-else>Upload in progress: {{ data.uploadOffset }} / {{ data.contentLength }} bytes</p>
        <p v-if="data.receivedChunks">Received chunks: {{ data.receivedChunks.length }}</p>
    </div>
</template>

Put File (Create/Update)

<script setup lang="ts">
import { ref } from "vue";
import { usePutFile } from "@visulima/storage-client/vue";

const props = defineProps<{ fileId: string }>();

const file = ref<File | null>(null);
const { putFile, progress, isLoading, error, data } = usePutFile({
    endpoint: "/api/files",
    onSuccess: (result) => {
        console.log("File uploaded:", result);
    },
});

const handleFileChange = (e: Event) => {
    const target = e.target as HTMLInputElement;
    file.value = target.files?.[0] || null;
};

const handleUpload = async () => {
    if (file.value) {
        await putFile(props.fileId, file.value);
    }
};
</script>

<template>
    <div>
        <input type="file" @change="handleFileChange" />
        <button @click="handleUpload" :disabled="!file || isLoading">
            {{ isLoading ? `Uploading... ${progress}%` : "Upload" }}
        </button>
        <div v-if="error">Error: {{ error.message }}</div>
        <div v-if="data">Upload complete! ID: {{ data.id }}</div>
    </div>
</template>

Batch Delete Files

<script setup lang="ts">
import { useBatchDeleteFiles } from "@visulima/storage-client/vue";

const { batchDeleteFiles, isLoading } = useBatchDeleteFiles({
    endpoint: "/api/files",
    onSuccess: (result) => {
        console.log(`Deleted ${result.successful} files`);
        if (result.failed) {
            console.log(`${result.failed} files failed to delete`);
        }
    },
});

const handleDelete = async () => {
    const fileIds = ["file1", "file2", "file3"];
    await batchDeleteFiles(fileIds);
};
</script>

<template>
    <button @click="handleDelete" :disabled="isLoading">
        {{ isLoading ? "Deleting..." : "Delete Selected Files" }}
    </button>
</template>

Transform Operations

Transform File

<script setup lang="ts">
import { ref, watch } from "vue";
import { useTransformFile } from "@visulima/storage-client/vue";

const props = defineProps<{ fileId: string }>();

const transform = ref({ width: 800, height: 600, quality: 85 });
const { data, isLoading, error, meta } = useTransformFile({
    endpoint: "/api/files",
    id: () => props.fileId,
    transform: () => transform.value,
});

const url = ref<string | null>(null);

watch(data, (blob) => {
    if (blob) {
        url.value = URL.createObjectURL(blob);
    }
});
</script>

<template>
    <div v-if="isLoading">Transforming...</div>
    <div v-else-if="error">Error: {{ error.message }}</div>
    <div v-else-if="data && url">
        <img :src="url" alt="Transformed" />
        <div>
            <label>
                Width:
                <input type="number" :value="transform.width" @input="transform.width = Number(($event.target as HTMLInputElement).value)" />
            </label>
            <label>
                Height:
                <input type="number" :value="transform.height" @input="transform.height = Number(($event.target as HTMLInputElement).value)" />
            </label>
            <label>
                Quality:
                <input
                    type="number"
                    min="1"
                    max="100"
                    :value="transform.quality"
                    @input="transform.quality = Number(($event.target as HTMLInputElement).value)"
                />
            </label>
        </div>
    </div>
</template>

Get Transform Metadata

<script setup lang="ts">
import { useTransformMetadata } from "@visulima/storage-client/vue";

const { data, isLoading, error } = useTransformMetadata({
    endpoint: "/api/files",
});
</script>

<template>
    <div v-if="isLoading">Loading...</div>
    <div v-else-if="error">Error: {{ error.message }}</div>
    <div v-else-if="data">
        <h3>Available Formats</h3>
        <ul>
            <li v-for="format in data.formats" :key="format">{{ format }}</li>
        </ul>
        <h3>Available Parameters</h3>
        <ul>
            <li v-for="param in data.parameters" :key="param">{{ param }}</li>
        </ul>
    </div>
</template>

Abort Operations

Abort All Uploads

<script setup lang="ts">
import { useAbortAll } from "@visulima/storage-client/vue";

const { abortAll } = useAbortAll({
    endpoint: "/api/upload/multipart",
});
</script>

<template>
    <button @click="abortAll">Abort All Uploads</button>
</template>

Abort Batch

<script setup lang="ts">
import { useAbortBatch } from "@visulima/storage-client/vue";

const props = defineProps<{ batchId: string }>();

const { abortBatch } = useAbortBatch({
    endpoint: "/api/upload/multipart",
});
</script>

<template>
    <button @click="() => abortBatch(props.batchId)">Abort Batch</button>
</template>

Abort Item

<script setup lang="ts">
import { useAbortItem } from "@visulima/storage-client/vue";

const props = defineProps<{ itemId: string }>();

const { abortItem } = useAbortItem({
    endpoint: "/api/upload/multipart",
});
</script>

<template>
    <button @click="() => abortItem(props.itemId)">Abort Upload</button>
</template>

Retry Operations

Retry Failed Upload

<script setup lang="ts">
import { useRetry } from "@visulima/storage-client/vue";

const props = defineProps<{ itemId: string }>();

const { retryItem } = useRetry({
    endpoint: "/api/upload/multipart",
});
</script>

<template>
    <button @click="() => retryItem(props.itemId)">Retry Upload</button>
</template>

Batch Retry

<script setup lang="ts">
import { useBatchRetry } from "@visulima/storage-client/vue";

const props = defineProps<{ batchId: string }>();

const { retryBatch } = useBatchRetry({
    endpoint: "/api/upload/multipart",
});
</script>

<template>
    <button @click="() => retryBatch(props.batchId)">Retry Failed Items in Batch</button>
</template>

Nuxt-Specific Features

Server-Side Setup

In Nuxt, you can set up upload endpoints using server routes:

// server/api/upload/multipart.post.ts
import { DiskStorage } from "@visulima/storage";
import { Multipart } from "@visulima/storage/handler/http/node";

const storage = new DiskStorage({ directory: "./uploads" });
const multipart = new Multipart({ storage });

export default defineEventHandler(async (event) => {
    event.node.res.setHeader("Access-Control-Allow-Origin", "*");
    await multipart.handle(event.node.req, event.node.res);
});

Using with Nuxt Module

If you're using the @visulima/storage Nuxt module, endpoints are automatically registered:

// nuxt.config.ts
import { DiskStorage } from "@visulima/storage";
import storageModule from "@visulima/storage/adapter/nuxt";

export default defineNuxtConfig({
    modules: [
        [
            storageModule,
            {
                storage: new DiskStorage({
                    directory: "./uploads",
                }),
            },
        ],
    ],
});

Then use the client in your components:

<script setup lang="ts">
import { useUpload } from "@visulima/storage-client/vue";

const { upload, progress, isUploading } = useUpload({
    endpointMultipart: "/api/upload/multipart",
    endpointTus: "/api/upload/tus",
});
</script>

Event Listeners

Batch Event Listeners

Listen to batch upload events:

<script setup lang="ts">
import { useBatchStartListener, useBatchProgressListener, useBatchFinishListener, useBatchErrorListener } from "@visulima/storage-client/vue";

useBatchStartListener({
    endpoint: "/api/upload/multipart",
    onBatchStart: (batchId) => {
        console.log("Batch started:", batchId);
    },
});

useBatchProgressListener({
    endpoint: "/api/upload/multipart",
    onBatchProgress: (progress, batchId) => {
        console.log(`Batch ${batchId} progress: ${progress}%`);
    },
});

useBatchFinishListener({
    endpoint: "/api/upload/multipart",
    onBatchFinish: (results, batchId) => {
        console.log(`Batch ${batchId} finished:`, results);
    },
});

useBatchErrorListener({
    endpoint: "/api/upload/multipart",
    onBatchError: (error, batchId) => {
        console.error(`Batch ${batchId} error:`, error);
    },
});
</script>

<template>
    <div>Upload component with listeners</div>
</template>

Retry Listener

<script setup lang="ts">
import { useRetryListener } from "@visulima/storage-client/vue";

useRetryListener({
    endpoint: "/api/upload/multipart",
    onRetry: (itemId) => {
        console.log("Retrying item:", itemId);
    },
});
</script>

<template>
    <!-- This is just a listener -->
</template>

All Abort Listener

<script setup lang="ts">
import { useAllAbortListener } from "@visulima/storage-client/vue";

useAllAbortListener({
    endpoint: "/api/upload/multipart",
    onAbortAll: () => {
        console.log("All uploads aborted");
    },
});
</script>

<template>
    <!-- This is just a listener -->
</template>

Next Steps

Support

Contribute to our work and keep us going

Community is the heart of open source. The success of our packages wouldn't be possible without the incredible contributions of users, testers, and developers who collaborate with us every day.Want to get involved? Here are some tips on how you can make a meaningful impact on our open source projects.

Ready to help us out?

Be sure to check out the package's contribution guidelines first. They'll walk you through the process on how to properly submit an issue or pull request to our repositories.

Submit a pull request

Found something to improve? Fork the repo, make your changes, and open a PR. We review every contribution and provide feedback to help you get merged.

Good first issues

Simple issues suited for people new to open source development, and often a good place to start working on a package.
View good first issues