#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
| Export | Type | Purpose |
debouncedWaitTime | const (250 or 1000) | Adaptive debounce for auto-complete inputs based on Save-Data header |
baseURL | const (string) | Server origin — localhost:8080 in dev, empty in prod |
baseRestURL | const (string) | baseURL + "/rs" — standard REST endpoint prefix |
createQueryParams(obj) | function → string | Builds key=val&key=val from object, filters undefined values |
evalServiceURL(url, params) | function → string | Appends query params to URL, handles existing ? vs & |
getServiceURL(url, params) | function → string | Full URL builder — prepends base + REST prefix, appends params |
handleHTTPErrors(res) | function → Response | Throws Error on non-2xx responses, passes through on success |
fetchJsonGet(url, params, cb) | function → Promise | GET with JSON response, parsed and passed to callback |
fetchJsonPost(url, body, cb) | function → Promise | POST with JSON-stringified body and JSON response |
fetchGet(url, params, cb) | function → Promise | Plain GET with optional callback (no JSON parsing) |
getObjectFromQuery(str) | function → object | Inverse 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
| Commit | What changed in this file |
bf988bc6d | React-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'. |
823ef0992 | Defensive 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. |
253b9f38b | Code 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. |
ac30e55f7 | Added 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. |
bbd81edc3 | ESLint-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
| Decision | Why | Trade-off |
No Axios, plain fetch | Zero 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-returning | Historical. 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 handling | Zero 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 debounce | Progressive 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. |