How RomM Frontend Communicates with the Backend Using REST and WebSockets
RomM's Vue 3 frontend communicates with its FastAPI backend through Axios for REST API operations and Socket.IO for real-time WebSocket events, with centralized service wrappers handling authentication, CSRF protection, and connection lifecycle management.
The RomM game library manager implements a clean separation between its Vue 3 single-page application frontend and Python backend using two complementary communication channels. Understanding how the RomM frontend communicates with the backend requires examining the Axios-based REST client for CRUD operations and the Socket.IO implementation for live push notifications. This architecture enables efficient data fetching while supporting real-time features like library scan progress and log streaming.
REST API Communication via Axios
RomM centralizes all HTTP communication in a configured Axios instance that handles authentication headers, request tracking, and error states.
Centralized Axios Client Configuration
The REST client resides in [frontend/src/services/api/index.ts](https://github.com/rommapp/romm/blob/master/frontend/src/services/api/index.ts) and exports a pre-configured Axios instance:
import axios from "axios";
import Cookies from "js-cookie";
const api = axios.create({
baseURL: "/api",
timeout: 120000,
paramsSerializer: { … }
});
The base URL of /api ensures all requests route through the Vite dev server proxy to the FastAPI backend. A CSRF token is automatically injected into every request via the x-csrftoken header, retrieved from the romm_csrftoken cookie using the js-cookie library.
Request Interceptors and Network State
The Axios instance implements request and response interceptors to track global network activity and handle authentication:
// Request interceptor
api.interceptors.request.use((config) => {
inflightRequests.add(config.url);
config.headers["x-csrftoken"] = Cookies.get("romm_csrftoken");
return config;
});
// Response interceptor
api.interceptors.response.use(
(response) => {
inflightRequests.delete(response.config.url);
document.dispatchEvent(new CustomEvent("backend-online"));
if (inflightRequests.size === 0) networkQuiesced();
return response;
},
(error) => {
// Error handling and backend state tracking
return Promise.reject(error);
}
);
The inflightRequests Set tracks active HTTP calls, enabling the UI to display loading states only when necessary. When the set empties, the system emits a network-quiesced event, allowing components to hide global spinners. Error responses with status codes ≥ 500 trigger backend-suspect flags, while 4xx responses maintain the backend-online state.
Fetching ROM Data
Components interact with the REST API through Pinia stores that call typed service functions. The [frontend/src/services/api/rom.ts](https://github.com/rommapp/romm/blob/master/frontend/src/services/api/rom.ts) module provides the interface:
import api from "./index";
export async function getRoms(params: Record<string, any>) {
const { data } = await api.get("/roms", { params });
return data;
}
The [frontend/src/stores/roms.ts](https://github.com/rommapp/romm/blob/master/frontend/src/stores/roms.ts) store consumes this service:
import { getRoms } from "@/services/api/rom";
export const useRomsStore = defineStore("roms", {
state: () => ({ allRoms: [] as SimpleRom[] }),
actions: {
async fetchRoms(filterParams) {
const result = await getRoms(filterParams);
this.allRoms = result.roms;
},
},
});
All REST responses are type-safe through TypeScript interfaces generated from the backend OpenAPI schema, located in frontend/src/__generated__/models/.
WebSocket Communication via Socket.IO
For real-time functionality, RomM uses Socket.IO to establish persistent connections that support bidirectional event streaming.
Socket.IO Client Configuration
The WebSocket client is configured in [frontend/src/services/socket.ts](https://github.com/rommapp/romm/blob/master/frontend/src/services/socket.ts):
import { io } from "socket.io-client";
export default io({
path: "/ws/socket.io/",
transports: ["websocket", "polling"],
autoConnect: false,
});
The path /ws/socket.io/ matches the FastAPI backend's WebSocket endpoint mount point. Setting autoConnect to false allows the UI to establish connections only when specific real-time features are active, conserving resources when idle. The transport fallback ensures compatibility by attempting WebSocket first, then reverting to HTTP polling if necessary.
Real-Time Event Handling
Components manually manage the socket lifecycle for operations like library scans. The [frontend/src/views/Scan/ScanPage.vue](https://github.com/rommapp/romm/blob/master/frontend/src/views/Scan/ScanPage.vue) demonstrates the pattern:
import socket from "@/services/socket";
function startScan() {
socket.connect();
socket.on("scan:update_stats", (stats) => {
scanStore.updateProgress(stats);
});
socket.on("scan:log", (msg) => {
scanStore.appendLog(msg);
});
socket.once("scan:stop", () => {
socket.disconnect();
scanStore.setComplete();
});
}
The backend emits three primary event types during scans:
scan:update_stats: Contains live counters for processed ROMs and platformsscan:log: Streams log messages as they are generatedscan:stop: Signals scan completion, triggering client-side cleanup
Connection Resilience
Socket.IO handles automatic reconnection with exponential backoff. The client can monitor connection health through built-in events:
socket.on("connect_error", (err) => {
console.warn("WebSocket error:", err);
// Trigger fallback UI or polling mode
});
All socket interactions are encapsulated in the default export from socket.ts, keeping the rest of the codebase agnostic to the underlying library implementation.
Complete User Flow Example
A typical interaction combining both channels occurs when browsing the gallery and initiating a scan:
- The Gallery page mounts and calls
fetchRoms()via the Pinia store - The Axios client executes
GET /api/roms?platform_id=...with automatic CSRF headers - The response updates the reactive store, triggering UI refresh
- The user initiates a library scan, calling
socket.connect() - The backend pushes progress via Socket.IO events while the REST API remains available for other operations
- Upon receiving
scan:stop, the client disconnects the socket and refreshes ROM data via REST
This separation ensures that long-running WebSocket operations do not block standard HTTP requests, maintaining UI responsiveness throughout the scanning process.
Summary
- Axios configuration: Centralized in
frontend/src/services/api/index.tswith baseURL/api, 120-second timeout, and automatic CSRF injection viax-csrftokenheaders - Network tracking: The
inflightRequestsSet monitors active HTTP calls to coordinate global loading states and backend health detection - Socket.IO setup: Configured in
frontend/src/services/socket.tswith path/ws/socket.io/and manual connection management to control resource usage - Event architecture: Real-time features use specific events (
scan:update_stats,scan:log,scan:stop) to stream progress without polling overhead - Type safety: Both channels leverage OpenAPI-generated TypeScript models located in
frontend/src/__generated__/models/
Frequently Asked Questions
How does RomM handle authentication for API requests?
RomM injects CSRF tokens automatically through Axios request interceptors. The x-csrftoken header is populated from the romm_csrftoken cookie on every request, while the backend validates session state for protected endpoints. This dual-layer approach prevents cross-site request forgery while maintaining session continuity.
What WebSocket events does RomM use for scan operations?
RomM emits three specific events during library scans: scan:update_stats transmits progress counters for processed files, scan:log streams real-time log output to the frontend, and scan:stop signals completion to trigger cleanup. These events are handled in the ScanPage component and stored in the scan Pinia store for reactive UI updates.
How does the frontend track when API requests are in flight?
The Axios instance maintains an inflightRequests Set that tracks URLs of active requests. Request interceptors add URLs to the set, while response interceptors remove them. When the set empties, the system emits a network-quiesced event, allowing components to hide loading indicators and enabling optimistic UI updates.
Why does RomM use both REST APIs and WebSockets instead of just one protocol?
RomM uses REST APIs for standard CRUD operations, configuration, and metadata retrieval because these are stateless, cacheable, and fit the request-response model. WebSockets handle real-time push notifications for long-running processes like library scans and log streaming because they eliminate polling overhead and provide immediate server-to-client updates without repeated HTTP requests.
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 →