Grabrgrabr
Back to home

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-node

2. 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 App

4. Run the Full Stack

You need to start both servers. Open two terminal windows.

Terminal 1 (Backend):

bash
cd backend
bun run server.ts

Terminal 2 (Frontend):

bash
cd frontend
bun run dev

Open 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.