# How the useApi Hook Manages REST API Calls in Modly

> Discover how the useApi hook in Modly centralizes HTTP communication with its FastAPI backend using a pre-configured Axios instance and typed helper functions.

- Repository: [lightningpixel/modly](https://github.com/lightningpixel/modly)
- Tags: how-to-guide
- Published: 2026-08-20

---

**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)](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.

```typescript
// 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.

```typescript
// 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`.

```typescript
// 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.

```typescript
// 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.

```typescript
// 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.

```typescript
// 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`](https://github.com/lightningpixel/modly/blob/main/src/shared/hooks/useApi.test.mjs))

## Practical Usage Examples

### Generating and Polling for Completion

```typescript
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

```typescript
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.