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 platforms
  • scan:log: Streams log messages as they are generated
  • scan: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:

  1. The Gallery page mounts and calls fetchRoms() via the Pinia store
  2. The Axios client executes GET /api/roms?platform_id=... with automatic CSRF headers
  3. The response updates the reactive store, triggering UI refresh
  4. The user initiates a library scan, calling socket.connect()
  5. The backend pushes progress via Socket.IO events while the REST API remains available for other operations
  6. 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.ts with baseURL /api, 120-second timeout, and automatic CSRF injection via x-csrftoken headers
  • Network tracking: The inflightRequests Set monitors active HTTP calls to coordinate global loading states and backend health detection
  • Socket.IO setup: Configured in frontend/src/services/socket.ts with 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:

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 →