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/statusreturns download state for a specific modelgetAllModelsStatus()—GET /model/allreturns 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
apiUrllives in Zustand, not environment variables or hard-coded strings - Request cancellation — Native
AbortSignalpropagation 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
useApicentralizes REST communication in Modly's React frontend, exposing typed helper functions while hiding Axios and request construction details- Dynamic configuration through Zustand's
apiUrlmakes 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →