EN · DE · RU · FR · ES

#2757: rest.js

projectforge-webapp/src/utilities/rest.js JavaScript / React utility — central REST API client for the ProjectForge React frontend. Source: projectforge-webapp/src/utilities/rest.js 88 lines · 68 code · 7 comments · 13 blank
Every React component that communicates with the ProjectForge backend goes through this module. It provides a thin wrapper around the fetch API with environment-aware URL construction, session-cookie authentication (credentials: 'include'), adaptive debouncing for slow connections, and JSON request/response handling. This is the single point of contact between the React SPA and the Spring Boot REST layer — there is no other HTTP client in the frontend.

Architecture

URL Construction Pipeline

The module builds REST URLs through a three-stage pipeline, each stage handling one concern:

Stage 1 — baseURL: Resolves the server origin. In development (import.meta.env.DEV), points to http://localhost:8080 — the Spring Boot dev server. In production, returns an empty string, making all URLs relative to the page origin (no CORS needed). The MODE !== 'test' guard prevents the test server URL from leaking into Jest tests.

Stage 2 — getServiceURL / baseRestURL: Prepends the REST prefix. If the service URL starts with / (absolute from root), uses baseURL directly. Otherwise, uses baseRestURL which appends /rs — the standard ProjectForge REST endpoint prefix. This allows both getServiceURL('/some/path') (absolute) and getServiceURL('user/list') (relative to /rs).

Stage 3 — evalServiceURL / createQueryParams: Appends query parameters. Filters out undefined values, encodes each value, and correctly uses ? vs & depending on whether the URL already contains a query string.

Debounce Strategy — saveData Awareness

The debouncedWaitTime constant adapts based on the Save-Data client hint (MDN). When the user has enabled data-saving mode (common on mobile), wait time increases from 250ms to 1000ms — reducing request frequency on metered connections. This is used by the auto-completion components (address lookup, user search, task search) to throttle API calls while typing.

The design is defensive: navigator && navigator.connection && navigator.connection.saveData — three guards before accessing the property, because navigator.connection is not available in all browsers (Firefox, Safari).

Exported API

ExportTypePurpose
debouncedWaitTimeconst (250 or 1000)Adaptive debounce for auto-complete inputs based on Save-Data header
baseURLconst (string)Server origin — localhost:8080 in dev, empty in prod
baseRestURLconst (string)baseURL + "/rs" — standard REST endpoint prefix
createQueryParams(obj)function → stringBuilds key=val&key=val from object, filters undefined values
evalServiceURL(url, params)function → stringAppends query params to URL, handles existing ? vs &
getServiceURL(url, params)function → stringFull URL builder — prepends base + REST prefix, appends params
handleHTTPErrors(res)function → ResponseThrows Error on non-2xx responses, passes through on success
fetchJsonGet(url, params, cb)function → PromiseGET with JSON response, parsed and passed to callback
fetchJsonPost(url, body, cb)function → PromisePOST with JSON-stringified body and JSON response
fetchGet(url, params, cb)function → PromisePlain GET with optional callback (no JSON parsing)
getObjectFromQuery(str)function → objectInverse of createQueryParams — parses ?key=val&... into object

Error Handling

All three fetch functions use the same error handling pattern: .catch((error) => alert('Internal error: ' + error)). The /* eslint-disable no-alert */ directive at the top acknowledges that alert() is normally forbidden by ESLint but is intentionally used here as the last-resort user notification.

This is a deliberate trade-off: a proper error toast system (Redux-driven, i18n-aware) would be better UX, but rest.js is the bottom of the dependency graph — it can't import Redux or React components. Using alert() means the error handler has zero dependencies and works in any context (even outside React's lifecycle). The file /* eslint-disable no-alert */ was added in commit ac30e55f7 to suppress the linting warning about this intentional choice.

Authentication

All fetch calls use credentials: 'include' — instructs the browser to send HTTP-only session cookies with every request. This is the standard authentication mechanism for SPAs served from the same origin as the API. The session cookie is set by the Spring Boot backend on login and automatically included by the browser. There is no token management, no Authorization header, and no client-side session logic — the browser handles it transparently via the credentials flag.

Usage in the Frontend

This module is imported by virtually every React component that fetches data. Typical usage patterns:
// In a React component:
import { fetchJsonGet, fetchJsonPost } from '../utilities/rest';

// Load a list
fetchJsonGet('user/list', { search: 'kai' }, (data) => setUsers(data));

// Save an entity
fetchJsonPost('user/save', { id: 1, username: 'kai' }, (data) => console.log('saved', data));

// Plain GET for a file download
fetchGet('export/excel', { id: 123 }, () => setDownloading(false));

Consumers

The module is imported across the entire React frontend — address book, calendar, timesheets, user management, polls, scripts, and all plugin UIs. At the time of writing, approximately 40+ React components import from this file. Any change to the URL construction logic (e.g., changing the REST prefix from /rs to something else) would affect every API call in the application.

Git History

CommitWhat changed in this file
bf988bc6dReact-scripts→Vite migration. Environment detection changed from process.env.NODE_ENV === 'development' (CRA convention) to import.meta.env.DEV && import.meta.env.MODE !== 'test' (Vite convention). The added MODE !== 'test' guard prevents Jest from pointing to localhost:8080 during test runs — a common bug when migrating from CRA to Vite where the test environment's mode is 'test', not 'development'.
823ef0992Defensive callback guard in fetchGet. Changed from .then(() => callback()) to .then(() => { if (typeof callback === 'function' && callback()); }). The fetchGet function has an optional callback — when called without one (e.g., fire-and-forget requests), the old code would throw TypeError: callback is not a function. The guard silently skips the callback when it's not a function.
253b9f38bCode formatting pass. Flattened multi-line fetch call chains into single-line calls. Before: fetch(\n getServiceURL(...), {\n method: 'GET',\n .... After: fetch(getServiceURL(...), { method: 'GET', .... Reduced visual noise, no behavioral change.
ac30e55f7Added ESLint suppression. Inserted /* eslint-disable no-alert */ at the top of the file. The error handlers use alert() for user-facing errors, which ESLint's recommended config forbids. The suppression acknowledges this is intentional — a zero-dependency error notification mechanism for a module at the bottom of the dependency graph.
bbd81edc3ESLint-driven formatting. Arrow functions changed from params => ... to (params) => ... (parenthesized single-parameter arrows). The redundant function body in getServiceURL was removed — the function was previously defined with function getServiceURL(...) { ... } and the rewrite was a formatting-only change.

Design Decisions

DecisionWhyTrade-off
No Axios, plain fetchZero dependencies. The module is ~80 lines and covers all the frontend's needs. Adding Axios (~14KB gzipped) for interceptors and automatic JSON parsing isn't justified for three HTTP methods.No request/response interceptors — auth token refresh would require custom code. Currently not needed because auth is cookie-based.
Callback-based instead of promise-returningHistorical. When this module was written (2019), the frontend used class components with this.setState in callbacks. Returning promises would require .then(data => this.setState(...)) in every consumer — the callback pattern was simpler at the time.Harder to chain, harder to use with async/await. Consumers must pass callbacks even when they just want the data. A modern rewrite would return promises.
alert() for error handlingZero dependencies, works in any context. A proper toast system would require importing React/Redux — creating a circular dependency risk.Ugly, not i18n-aware, blocks the UI thread. A deferred error event emitter would be better: window.dispatchEvent(new CustomEvent('rest:error', {detail: error})) — consumed by a React error boundary or toast component.
saveData-aware debounceProgressive enhancement. Users with metered connections (mobile, developing countries) get slower debounce — fewer API calls, less data usage. Users with unmetered connections get faster debounce — snappier autocomplete.Browser support is limited — navigator.connection is Chromium-only. Firefox/Safari users always get the 250ms default. Graceful degradation works correctly.