How the useApi Hook Manages REST API Calls in Modly

The useApi hook centralizes all HTTP communication with Modly's FastAPI backend by creating a pre-configured Axios instance and exposing typed helper functions for generation, polling, model management, and mesh operations.

The Modly application uses a custom React hook called useApi to handle every REST API interaction between its React-based UI and the Python FastAPI backend. This hook eliminates direct HTTP logic from components, providing a clean abstraction layer that manages request configuration, payload formatting, error handling, and response normalization. According to the lightningpixel/modly source code, the implementation combines Zustand for dynamic configuration management with Axios for the actual transport layer.

How useApi Initializes the HTTP Client

Retrieving the API Base URL from Global State

The hook does not hard-code backend addresses. Instead, it reads apiUrl from the global Zustand store defined in [src/shared/stores/appStore.ts](https://github.com/lightningpixel/modly/blob/main/src/shared/stores/appStore.ts). This value is populated when the backend process starts, making the client adaptable across development, staging, and packaged production environments.

// From appStore.ts (lines 78-80)
apiUrl: get().apiUrl,  // Set dynamically when FastAPI server starts

Creating the Axios Instance

With the URL retrieved, useApi creates a single Axios instance with baseURL pre-configured. All subsequent API calls reuse this client, ensuring consistent headers, timeout handling, and interceptors.

// From useApi.ts (lines 5-8)
const api = axios.create({
  baseURL: apiUrl,
});

Core API Helper Functions

The useApi hook returns an object containing specialized async functions, each mapping to specific backend routes. These hide request construction details from UI components.

Image Generation with Multipart Uploads

The generateFromImage function handles the most complex request type. It accepts either a base64 string or reads a file via Electron's IPC bridge (window.electron.fs.readFileBase64), then constructs a FormData payload for POST /generate/from-image.

// Simplified from useApi.ts lines 9-34
const generateFromImage = async (
  imagePathOrBase64: string,
  options: GenerationOptions,
  signal?: AbortSignal
) => {
  let base64 = imagePathOrBase64;
  if (!imagePathOrBase64.startsWith('data:')) {
    base64 = await window.electron.fs.readFileBase64(imagePathOrBase64);
  }
  
  const formData = new FormData();
  formData.append('image', dataURItoBlob(base64));
  formData.append('options', JSON.stringify(options));
  
  const response = await api.post('/generate/from-image', formData, {
    headers: { 'Content-Type': 'multipart/form-data' },
    signal,  // AbortSignal support for cancellation
  });
  
  return { jobId: response.data.job_id };
};

Key features include:

  • AbortSignal support — enables request cancellation when users abort generation
  • Automatic format detection — distinguishes between data URIs and file paths
  • Electron IPC integration — reads local files securely without exposing filesystem access to the renderer

Job Status Polling

The pollJobStatus function queries GET /generate/status/:jobId and normalizes snake_case response fields to camelCase for the frontend.

// From useApi.ts lines 36-45
const pollJobStatus = async (jobId: string) => {
  const response = await api.get(`/generate/status/${jobId}`);
  return {
    ...response.data,
    outputUrl: response.data.output_url,  // Normalization
  };
};

Model Management Operations

Two lightweight getters handle model availability:

  • getModelStatus() — GET /model/status returns download state for a specific model
  • getAllModelsStatus() — GET /model/all returns status for all available models

Streaming Model Downloads

The downloadModel function demonstrates advanced streaming handling. It initiates GET /model/download and processes Server-Sent Events-style JSON lines embedded in the response stream.

// From useApi.ts lines 62-86
const downloadModel = async (onProgress?: (pct: number) => void) => {
  const response = await api.get('/model/download', {
    responseType: 'stream',
    adapter: 'http',
  });
  
  return new Promise<void>((resolve, reject) => {
    response.data.on('data', (chunk: Buffer) => {
      const lines = chunk.toString().split('\n');
      for (const line of lines) {
        if (line.startsWith('data:')) {
          const data = JSON.parse(line.slice(5));
          if (data.progress && onProgress) {
            onProgress(data.progress);
          }
        }
      }
    });
    response.data.on('end', resolve);
    response.data.on('error', reject);
  });
};

This approach provides real-time progress updates without polling overhead, parsing data: {progress} lines directly from the Node.js stream.

Mesh Processing Pipeline

Four functions handle 3D mesh operations, each sending JSON payloads to /optimize/* endpoints:

Function Endpoint Purpose
optimizeMesh POST /optimize/reduce Polygon reduction to target face count
smoothMesh POST /optimize/smooth Laplacian smoothing
importMesh POST /optimize/import Format conversion and validation
transformMesh POST /optimize/transform Rotation, scaling, translation

All return { url: string, faceCount: number } shapes for consistent downstream handling.

Silent Cancellation

The cancelJob function fires POST /generate/cancel/:jobId with error suppression, ensuring UI flow remains uninterrupted even if the cancellation request fails.

// From useApi.ts lines 99-101
const cancelJob = async (jobId: string) => {
  await api.post(`/generate/cancel/${jobId}`).catch(() => {});
};

Architectural Benefits of the useApi Hook

The implementation in lightningpixel/modly delivers several critical architectural advantages:

  • Configuration externalization — The apiUrl lives in Zustand, not environment variables or hard-coded strings
  • Request cancellation — Native AbortSignal propagation through Axios enables clean request termination
  • Streaming without dependencies — Custom SSE parsing avoids adding dedicated event-source libraries
  • Type-safe contracts — Every helper returns predictable TypeScript shapes, eliminating defensive coding in components
  • Testable boundaries — The hook's interface allows straightforward mocking in unit tests (see useApi.test.mjs)

Practical Usage Examples

Generating and Polling for Completion

import { useApi } from '@shared/hooks/useApi';
import { useEffect, useState } from 'react';

function GenerationFlow({ imagePath }: { imagePath: string }) {
  const { generateFromImage, pollJobStatus } = useApi();
  const [jobId, setJobId] = useState<string | null>(null);
  const [result, setResult] = useState<GenerationResult | null>(null);

  const startGeneration = async () => {
    const controller = new AbortController();
    const { jobId } = await generateFromImage(
      imagePath,
      { modelId: 'stable-diffusion', remesh: 'quad' },
      controller.signal
    );
    setJobId(jobId);
  };

  useEffect(() => {
    if (!jobId) return;
    
    const poll = setInterval(async () => {
      const status = await pollJobStatus(jobId);
      if (status.status === 'done') {
        setResult(status);
        clearInterval(poll);
      }
    }, 1500);
    
    return () => clearInterval(poll);
  }, [jobId]);

  return result ? <ModelViewer url={result.outputUrl} /> : <button onClick={startGeneration}>Generate</button>;
}

Monitoring Model Download Progress

function ModelDownloader() {
  const { downloadModel } = useApi();
  const [progress, setProgress] = useState(0);

  const handleDownload = async () => {
    await downloadModel(setProgress);
  };

  return (
    <>
      <button onClick={handleDownload} disabled={progress > 0 && progress < 100}>
        Download Model
      </button>
      {progress > 0 && <progress value={progress} max="100" />}
    </>
  );
}

Summary

  • useApi centralizes REST communication in Modly's React frontend, exposing typed helper functions while hiding Axios and request construction details
  • Dynamic configuration through Zustand's apiUrl makes the hook environment-agnostic without rebuilds
  • Advanced features include AbortSignal cancellation, streaming SSE progress parsing, and automatic field normalization
  • Clean separation of concerns keeps UI components focused on presentation rather than HTTP mechanics

Frequently Asked Questions

How does useApi handle request cancellation?

useApi forwards an optional AbortSignal from caller to Axios's signal option, most notably in generateFromImage. This allows components to cancel in-flight requests when users abort operations, avoiding stale state updates from completed-but-irrelevant network calls.

Why does Modly use Zustand instead of environment variables for the API URL?

The backend URL isn't known at build time—it depends on which port the Python FastAPI process successfully binds to on the user's machine. Storing apiUrl in the Zustand store allows runtime discovery and immediate propagation across all components without page reloads.

What makes the downloadModel streaming implementation unusual?

Rather than using a dedicated Server-Sent Events library, downloadModel consumes the raw Node.js response stream and manually parses data: prefixed JSON lines. This lightweight approach achieves real-time progress feedback with zero additional dependencies.

Can I use useApi outside React components?

The hook relies on useAppStore to retrieve apiUrl, which requires React's render context. For non-React usage, you would need to access the store's getState method directly or extract the Axios creation logic into a standalone factory function.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →