Using grabr with Vite + React
Because grabr performs aggressive parallel chunking and raw filesystem writes, it requires a Node.js or Bun backend environment. It cannot run directly inside a Vite React SPA.
In this tutorial, we will build a full-stack application. We will create an Express.js API backend to run grabr, and a Vite React frontend that tells the backend what files to download.
1. Project Setup
bash
mkdir my-grabr-app
cd my-grabr-app
bun create vite frontend -- --template react-ts
cd frontend
bun install
cd ..
mkdir backend
cd backend
bun init -y
bun add express cors @linuxctrl/grabr
bun add -D typescript @types/express @types/cors ts-node2. Build the Express Backend
Create backend/server.ts:
typescript
import express from "express";
import cors from "cors";
import path from "path";
import fs from "fs";
import { Downloader } from "@linuxctrl/grabr";
const app = express();
app.use(cors());
app.use(express.json());
const downloader = new Downloader();
downloader.start();
// Track progress server-side
downloader.on("job:progress", ({ jobId, filename, speed, eta, downloadedBytes, totalBytes }) => {
const pct = totalBytes > 0 ? Math.round(downloadedBytes / totalBytes * 100) : 0;
console.log(`${filename}: ${pct}% at ${Math.round(speed / 1e6)} MB/s`);
});
downloader.on("job:status", ({ jobId, status, filename }) => {
console.log(`${filename}: ${status}`);
});
app.post("/api/download", async (req, res) => {
const { url, filename } = req.body;
const outputDir = path.join(process.cwd(), "downloads");
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
try {
const job = await downloader.addJob(url, {
outputDir,
filename,
chunks: 8,
});
console.log(`Job added: ${job.filename}`);
res.json({ success: true, jobId: job.id });
} catch (error) {
console.error("Error starting grabr:", error);
res.status(500).json({ success: false, error: String(error) });
}
});
// Cleanup on shutdown
process.on("SIGINT", () => {
downloader.stop();
process.exit(0);
});
const PORT = 3001;
app.listen(PORT, () => {
console.log(`Backend running on http://localhost:${PORT}`);
});3. Build the Vite React Frontend
Open frontend/src/App.tsx and replace it with:
typescript
import { useState } from 'react'
function App() {
const [url, setUrl] = useState("https://speed.hetzner.de/100MB.bin");
const [status, setStatus] = useState("");
const handleDownload = async () => {
setStatus("Connecting to backend...");
try {
const response = await fetch("http://localhost:3001/api/download", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url, filename: "test-file.bin" })
});
const data = await response.json();
if (data.success) {
setStatus("Downloading! Check the backend terminal and downloads folder.");
} else {
setStatus(`Error: ${data.error}`);
}
} catch (err) {
setStatus("Failed to connect to backend");
}
};
return (
<main style={{ padding: "40px", fontFamily: "sans-serif" }}>
<h1>Grabr Vite Demo</h1>
<div style={{ display: "flex", gap: "10px", marginTop: "20px" }}>
<input
type="text"
value={url}
onChange={(e) => setUrl(e.target.value)}
style={{ width: "300px", padding: "8px" }}
/>
<button
onClick={handleDownload}
style={{ padding: "8px 16px", cursor: "pointer" }}
>
Download via Backend
</button>
</div>
{status && <p style={{ marginTop: "20px", fontWeight: "bold" }}>{status}</p>}
</main>
)
}
export default App4. Run the Full Stack
You need to start both servers. Open two terminal windows.
Terminal 1 (Backend):
bash
cd backend
bun run server.tsTerminal 2 (Frontend):
bash
cd frontend
bun run devOpen the Vite frontend URL in your browser, click Download via Backend, and watch your Express terminal!
Advanced:For real-time progress bars in React, you can connect to grabr's built-in WebSocket at
ws://localhost:7474/ws when running the daemon with grabr daemon start. No socket.io needed — the daemon broadcasts job:progress andjob:status events to all connected clients.