Storage ClientSolid Start

Solid Start

Use the storage client with Solid Start

Solid Start

Use @visulima/storage-client with Solid Start for full-stack SolidJS applications with server-side file handling and client-side uploads.

Installation

npm install @visulima/storage-client @tanstack/solid-query solid-js @solidjs/start

Setup QueryClient Provider

Configure the TanStack Solid Query provider in your root layout:

// src/app.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";

const queryClient = new QueryClient();

export default function App() {
    return (
        <QueryClientProvider client={queryClient}>
            <Router root={(props) => <>{props.children}</>}>
                <FileRoutes />
            </Router>
        </QueryClientProvider>
    );
}

Upload Component

Create a page with file upload functionality:

// src/routes/index.tsx
import { createUpload } from "@visulima/storage-client/solid";
import { createSignal } from "solid-js";

export default function Home() {
    const [file, setFile] = createSignal<File | null>(null);
    let fileInputRef: HTMLInputElement | undefined;

    const { error, isUploading, progress, result, upload, reset } = createUpload({
        endpointMultipart: "/api/upload/multipart",
        endpointTus: "/api/upload/tus",
        onSuccess: (result) => {
            console.log("Upload successful:", result);
            setFile(null);
            if (fileInputRef) {
                fileInputRef.value = "";
            }
        },
        onError: (error_) => {
            console.error("Upload error:", error_);
        },
    });

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

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

    const handleReset = () => {
        reset();
        setFile(null);
        if (fileInputRef) {
            fileInputRef.value = "";
        }
    };

    return (
        <main style={{ margin: "0 auto", "max-width": "800px", padding: "2rem" }}>
            <h1>Storage Client - Solid Start Example</h1>
            <p>Upload files using @visulima/storage-client with Solid Start</p>

            <div style={{ "margin-top": "2rem" }}>
                <input disabled={isUploading()} onChange={handleFileChange} ref={fileInputRef} type="file" />
                <button disabled={!file() || isUploading()} onClick={handleUpload} style={{ "margin-left": "1rem" }}>
                    {isUploading() ? `Uploading... ${progress()}%` : "Upload"}
                </button>
                <button disabled={isUploading()} onClick={handleReset} style={{ "margin-left": "0.5rem" }}>
                    Reset
                </button>
            </div>

            {isUploading() && (
                <div style={{ "margin-top": "1rem" }}>
                    <div>
                        Progress: {progress()}%
                    </div>
                    <progress max={100} value={progress()} style={{ width: "100%", "max-width": "400px" }} />
                </div>
            )}

            {error() && (
                <div style={{ color: "red", "margin-top": "1rem" }}>
                    Error: {error()?.message}
                </div>
            )}

            {result() && (
                <div style={{ color: "green", "margin-top": "1rem" }}>
                    <div>Upload complete!</div>
                    <div>File ID: {result()?.id}</div>
                    {result()?.filename && <div>Filename: {result()?.filename}</div>}
                    {result()?.url && (
                        <div>
                            URL: <a href={result()?.url} target="_blank" rel="noopener noreferrer">{result()?.url}</a>
                        </div>
                    )}
                </div>
            )}
        </main>
    );
}

Server API Routes

Multipart Upload Endpoint

// src/routes/api/upload/multipart.ts
import type { APIEvent } from "@solidjs/start/server";
import { createHandler } from "@visulima/storage/handler/http/fetch";
import { storage } from "~/lib/storage";

const handler = createHandler({ storage, type: "multipart" });

export async function POST(event: APIEvent) {
    return handler.fetch(event.request);
}

export async function DELETE(event: APIEvent) {
    return handler.fetch(event.request);
}

export async function GET(event: APIEvent) {
    return handler.fetch(event.request);
}

export async function OPTIONS(event: APIEvent) {
    return handler.fetch(event.request);
}

TUS Upload Endpoint

// src/routes/api/upload/tus.ts
import type { APIEvent } from "@solidjs/start/server";
import { createHandler } from "@visulima/storage/handler/http/fetch";
import { storage } from "~/lib/storage";

const handler = createHandler({ storage, type: "tus" });

export async function POST(event: APIEvent) {
    return handler.fetch(event.request);
}

export async function PATCH(event: APIEvent) {
    return handler.fetch(event.request);
}

export async function HEAD(event: APIEvent) {
    return handler.fetch(event.request);
}

export async function OPTIONS(event: APIEvent) {
    return handler.fetch(event.request);
}

TUS Upload by ID Endpoint

// src/routes/api/upload/tus/[id].ts
import type { APIEvent } from "@solidjs/start/server";
import { createHandler } from "@visulima/storage/handler/http/fetch";
import { storage } from "~/lib/storage";

const handler = createHandler({ storage, type: "tus" });

export async function PATCH(event: APIEvent) {
    return handler.fetch(event.request);
}

export async function HEAD(event: APIEvent) {
    return handler.fetch(event.request);
}

export async function DELETE(event: APIEvent) {
    return handler.fetch(event.request);
}

export async function OPTIONS(event: APIEvent) {
    return handler.fetch(event.request);
}

Chunked REST Upload Endpoint

// src/routes/api/upload/chunked-rest.ts
import type { APIEvent } from "@solidjs/start/server";
import { createHandler } from "@visulima/storage/handler/http/fetch";
import { storage } from "~/lib/storage";

const handler = createHandler({ storage, type: "rest" });

export async function POST(event: APIEvent) {
    return handler.fetch(event.request);
}

export async function OPTIONS(event: APIEvent) {
    return handler.fetch(event.request);
}
// src/routes/api/upload/chunked-rest/[id].ts
import type { APIEvent } from "@solidjs/start/server";
import { createHandler } from "@visulima/storage/handler/http/fetch";
import { storage } from "~/lib/storage";

const handler = createHandler({ storage, type: "rest" });

export async function PUT(event: APIEvent) {
    return handler.fetch(event.request);
}

export async function PATCH(event: APIEvent) {
    return handler.fetch(event.request);
}

export async function HEAD(event: APIEvent) {
    return handler.fetch(event.request);
}

export async function OPTIONS(event: APIEvent) {
    return handler.fetch(event.request);
}

Storage Configuration

Create a shared storage instance:

// src/lib/storage.ts
import { DiskStorage } from "@visulima/storage";

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

TUS Resumable Upload Example

For large file uploads with pause/resume support:

// src/routes/upload-large.tsx
import { createTusUpload } from "@visulima/storage-client/solid";
import { createSignal } from "solid-js";

export default function LargeFileUpload() {
    const [file, setFile] = createSignal<File | null>(null);

    const { upload, pause, resume, progress, isUploading, isPaused, offset, error, result, reset } = createTusUpload({
        endpoint: "/api/upload/tus",
        chunkSize: 1024 * 1024, // 1MB chunks
        onSuccess: (result) => {
            console.log("Upload complete:", result);
        },
    });

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

    const handleUpload = async () => {
        const currentFile = file();
        if (currentFile) {
            await upload(currentFile);
        }
    };

    return (
        <main style={{ padding: "2rem" }}>
            <h1>Large File Upload (TUS)</h1>
            <input type="file" onChange={handleFileChange} disabled={isUploading()} />
            <button onClick={handleUpload} disabled={!file() || isUploading()}>
                Upload
            </button>

            {isUploading() && (
                <div style={{ "margin-top": "1rem" }}>
                    <progress max={100} value={progress()} style={{ width: "100%" }} />
                    <div>Progress: {progress()}% ({offset()} bytes uploaded)</div>
                    {isPaused() ? (
                        <button onClick={resume}>Resume</button>
                    ) : (
                        <button onClick={pause}>Pause</button>
                    )}
                </div>
            )}

            {error() && <div style={{ color: "red" }}>Error: {error()?.message}</div>}
            {result() && <div style={{ color: "green" }}>Upload complete! ID: {result()?.id}</div>}
        </main>
    );
}

Batch Upload with Drag and Drop

// src/routes/batch-upload.tsx
import { createBatchUpload, createFileInput } from "@visulima/storage-client/solid";

export default function BatchUploadPage() {
    const { files, inputRef, handleFileChange, handleDrop, handleDragOver, openFileDialog, reset: resetFiles } = createFileInput({
        multiple: true,
    });

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

    const handleUpload = () => {
        const currentFiles = files();
        if (currentFiles.length > 0) {
            uploadBatch(currentFiles);
        }
    };

    return (
        <main style={{ padding: "2rem" }}>
            <h1>Batch Upload</h1>

            <div
                onDrop={handleDrop}
                onDragOver={handleDragOver}
                style={{
                    border: "2px dashed #ccc",
                    padding: "2rem",
                    "text-align": "center",
                    "border-radius": "8px",
                }}
            >
                <input ref={inputRef} type="file" multiple onChange={handleFileChange} style={{ display: "none" }} />
                <p>Drag and drop files here or</p>
                <button onClick={openFileDialog}>Select Files</button>
            </div>

            {files().length > 0 && (
                <div style={{ "margin-top": "1rem" }}>
                    <p>Selected: {files().length} files</p>
                    <button onClick={handleUpload} disabled={isUploading()}>
                        {isUploading() ? "Uploading..." : "Upload All"}
                    </button>
                </div>
            )}

            {isUploading() && (
                <div style={{ "margin-top": "1rem" }}>
                    <progress max={100} value={progress()} style={{ width: "100%" }} />
                    <div>Progress: {progress()}%</div>
                    <div>Completed: {completedCount()} / {items().length}</div>
                    <div>Errors: {errorCount()}</div>
                </div>
            )}
        </main>
    );
}

File Management

Listing and Deleting Files

// src/routes/files.tsx
import { createGetFileList, createDeleteFile, createBatchDeleteFiles } from "@visulima/storage-client/solid";
import { createSignal, For } from "solid-js";

export default function FilesPage() {
    const [selectedIds, setSelectedIds] = createSignal<string[]>([]);

    const { data, isLoading, refetch } = createGetFileList({
        endpoint: "/api/files",
    });

    const { deleteFile, isLoading: isDeleting } = createDeleteFile({
        endpoint: "/api/files",
    });

    const { batchDeleteFiles, isLoading: isBatchDeleting } = createBatchDeleteFiles({
        endpoint: "/api/files",
    });

    const handleDelete = async (id: string) => {
        await deleteFile(id);
        refetch();
    };

    const handleBatchDelete = async () => {
        await batchDeleteFiles(selectedIds());
        setSelectedIds([]);
        refetch();
    };

    const toggleSelect = (id: string) => {
        setSelectedIds((prev) =>
            prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id]
        );
    };

    return (
        <main style={{ padding: "2rem" }}>
            <h1>Files</h1>

            {selectedIds().length > 0 && (
                <button onClick={() => handleBatchDelete()} disabled={isBatchDeleting()}>
                    {isBatchDeleting() ? "Deleting..." : `Delete Selected (${selectedIds().length})`}
                </button>
            )}

            {isLoading() && <div>Loading...</div>}

            <ul>
                <For each={data()?.data}>
                    {(file) => (
                        <li>
                            <input
                                type="checkbox"
                                checked={selectedIds().includes(file.id)}
                                onChange={() => toggleSelect(file.id)}
                            />
                            {file.originalName || file.name || file.id}
                            {" "}({file.size} bytes)
                            <button onClick={() => handleDelete(file.id)} disabled={isDeleting()}>
                                Delete
                            </button>
                        </li>
                    )}
                </For>
            </ul>
        </main>
    );
}

Environment Variables

Configure storage backend via environment variables:

# .env
UPLOAD_DIR=./uploads

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