React SPA + StorageVault (S3 Static Hosting)

    In this tutorial, we will build a React SPA application that uses Pilvio StorageVault (S3) for hosting static files and uploading files.

    ⏳ CDN coming soon: Pilvio CDN is under development. For now, static files can be served directly from StorageVault's S3. Once CDN becomes available, you can easily add caching and geographic distribution.

    What we will build

    • A React SPA that communicates with a Pilvio backend
    • File uploads directly to StorageVault (presigned URLs)
    • Static deploy to an S3 bucket

    Prerequisites


    Step 1: Create the React project

    npm create vite@latest pilvio-react-app -- --template react
    cd pilvio-react-app
    npm install
    npm install axios
    

    Step 2: Create the Pilvio API client

    Create the file src/lib/pilvio-client.js:

    import axios from 'axios';
    
    // Backend API URL (mitte otse Pilvio API, vaid sinu backend)
    const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000';
    
    const api = axios.create({
      baseURL: API_BASE,
      headers: {
        'Content-Type': 'application/json',
      },
    });
    
    // Failide üleslaadimine backend proxy kaudu
    export async function uploadFile(file) {
      const formData = new FormData();
      formData.append('file', file);
    
      const response = await api.post('/api/v1/files/upload', formData, {
        headers: { 'Content-Type': 'multipart/form-data' },
      });
      return response.data;
    }
    
    // Failide loetelu
    export async function listFiles(prefix = 'uploads/') {
      const response = await api.get('/api/v1/files', {
        params: { prefix },
      });
      return response.data;
    }
    
    // Allalaadimis-URL hankimine (eelsigneeritud S3 URL)
    export async function getDownloadUrl(fileKey) {
      const response = await api.get(`/api/v1/files/download-url/${fileKey}`);
      return response.data;
    }
    
    // Faili kustutamine
    export async function deleteFile(fileKey) {
      const response = await api.delete(`/api/v1/files/${fileKey}`);
      return response.data;
    }
    
    export default api;
    

    Security: Never put Pilvio API tokens or S3 keys into a React application! All Pilvio API requests must go through your backend server.

    Step 3: File management component

    Create the file src/components/FileManager.jsx:

    import { useState, useEffect } from 'react';
    import { uploadFile, listFiles, getDownloadUrl, deleteFile } from '../lib/pilvio-client';
    
    export default function FileManager() {
      const [files, setFiles] = useState([]);
      const [uploading, setUploading] = useState(false);
      const [error, setError] = useState(null);
    
      useEffect(() => {
        loadFiles();
      }, []);
    
      async function loadFiles() {
        try {
          const data = await listFiles();
          setFiles(data.files || []);
        } catch (err) {
          setError('Failide laadimine ebaõnnestus');
        }
      }
    
      async function handleUpload(event) {
        const file = event.target.files[0];
        if (!file) return;
    
        setUploading(true);
        setError(null);
    
        try {
          await uploadFile(file);
          await loadFiles();
        } catch (err) {
          setError('Üleslaadimine ebaõnnestus');
        } finally {
          setUploading(false);
        }
      }
    
      async function handleDownload(fileKey) {
        try {
          const data = await getDownloadUrl(fileKey);
          window.open(data.url, '_blank');
        } catch (err) {
          setError('Allalaadimine ebaõnnestus');
        }
      }
    
      async function handleDelete(fileKey) {
        if (!confirm('Kas oled kindel?')) return;
        try {
          await deleteFile(fileKey);
          await loadFiles();
        } catch (err) {
          setError('Kustutamine ebaõnnestus');
        }
      }
    
      function formatSize(bytes) {
        if (bytes < 1024) return `${bytes} B`;
        if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
        return `${(bytes / 1048576).toFixed(1)} MB`;
      }
    
      return (
        <div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
          <h1>StorageVault failihaldus</h1>
    
          {error && (
            <div style={{ color: 'red', padding: '10px', marginBottom: '10px' }}>
              {error}
            </div>
          )}
    
          <div style={{ marginBottom: '20px' }}>
            <input type="file" onChange={handleUpload} disabled={uploading} />
            {uploading && <span> Laadin üles...</span>}
          </div>
    
          <table style={{ width: '100%', borderCollapse: 'collapse' }}>
            <thead>
              <tr>
                <th style={{ textAlign: 'left', padding: '8px' }}>Fail</th>
                <th style={{ textAlign: 'right', padding: '8px' }}>Suurus</th>
                <th style={{ textAlign: 'right', padding: '8px' }}>Tegevused</th>
              </tr>
            </thead>
            <tbody>
              {files.map((file) => (
                <tr key={file.key} style={{ borderTop: '1px solid #eee' }}>
                  <td style={{ padding: '8px' }}>{file.key.split('/').pop()}</td>
                  <td style={{ textAlign: 'right', padding: '8px' }}>{formatSize(file.size)}</td>
                  <td style={{ textAlign: 'right', padding: '8px' }}>
                    <button onClick={() => handleDownload(file.key)}>Lae alla</button>{' '}
                    <button onClick={() => handleDelete(file.key)}>Kustuta</button>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
    
          {files.length === 0 && <p>Faile ei ole. Laadi midagi üles!</p>}
        </div>
      );
    }
    

    Step 4: Environment variables

    Create the file .env:

    VITE_API_URL=https://sinu-api-domeen.ee
    

    Step 5: Static deploy to StorageVault (S3)

    Build the React application and upload it to an S3 bucket:

    # Ehita
    npm run build
    
    # Laadi dist/ sisu S3-sse (AWS CLI Pilvio endpointiga)
    aws s3 sync dist/ s3://minu-react-app/ \
      --endpoint-url https://s3.pilvio.com:8080 \
      --delete
    
    # Sea index.html Content-Type
    aws s3 cp s3://minu-react-app/index.html s3://minu-react-app/index.html \
      --endpoint-url https://s3.pilvio.com:8080 \
      --content-type "text/html" \
      --metadata-directive REPLACE
    

    Alternative: Deploy script

    Create the file deploy.sh:

    #!/bin/bash
    set -e
    
    echo "Ehitan React rakendust..."
    npm run build
    
    echo "Laadin üles StorageVault'i..."
    aws s3 sync dist/ s3://minu-react-app/ \
      --endpoint-url https://s3.pilvio.com:8080 \
      --delete \
      --cache-control "public, max-age=31536000" \
      --exclude "index.html"
    
    # index.html ilma vahemäluta (et uus versioon oleks kohe näha)
    aws s3 cp dist/index.html s3://minu-react-app/index.html \
      --endpoint-url https://s3.pilvio.com:8080 \
      --content-type "text/html" \
      --cache-control "no-cache"
    
    echo "Deploy valmis!"
    
    chmod +x deploy.sh
    ./deploy.sh
    

    Alternative: SPA hosting on Pilvio VM + Nginx

    If setting up S3 static hosting is complex, you can also serve the SPA via Nginx on a VM:

    # Kopeeri ehitatud failid serverisse
    scp -r dist/* deploy@SINU_FLOATING_IP:/var/www/react-app/
    
    # Nginx seadistus SPA jaoks
    sudo tee /etc/nginx/sites-available/react-app <<'EOF'
    server {
        listen 80;
        server_name sinu-domeen.ee;
        root /var/www/react-app;
        index index.html;
    
        location / {
            try_files $uri $uri/ /index.html;
        }
    
        location /assets/ {
            expires 1y;
            add_header Cache-Control "public, immutable";
        }
    }
    EOF
    

    ⏳ CDN (coming soon): Pilvio CDN will allow you to add automatic caching and edge serving to your S3 bucket in the future. The current S3-based solution works well, and adding CDN will only require changing the URL.

    Next steps: Connect with a backend API (Node.js or FastAPI) and add a database.