Skip to content

Frontend Architecture Overview

Paulo Gomes da Cruz Junior edited this page Oct 23, 2026 · 14 revisions

Architecture Overview — Waste Plus Frontend

Application Identity

Attribute Value
Name Waste Plus
Stack React 19 · TypeScript 5.9 · Vite 7 · TanStack Query 5 · TanStack Router 1
UI Kit Carbon Design System (IBM)
Auth AWS Amplify 6 + Cognito (IDIR / BCeID)
State React Context + notification event pipeline + TanStack Query server cache
Testing Vitest (unit) · Playwright (E2E / a11y)
PWA vite-plugin-pwa + Workbox (scaffolded, with lifecycle notifications wired)

Cross-Cutting References

  • Feature Flags: shared guidance for rollout strategy, operational safeguards, and lifecycle governance of toggled behavior.

React Architecture

Component Hierarchy

graph TD
  Root["main.tsx — StrictMode"]
  Root --> AuthProv["AuthProvider"]
  AuthProv --> QCP["QueryClientProvider"]
  QCP --> PrefProv["PreferenceProvider"]
  PrefProv --> ThemeProv["ThemeProvider"]
  ThemeProv --> NotifProv["NotificationProvider"]
  NotifProv --> PageTitleProv["PageTitleProvider"]
  PageTitleProv --> App["App"]
  App --> RP["RouterProvider (router singleton from router.tsx)"]
  App --> RQDevtools["ReactQueryDevtools"]
  RP --> RootRoute["__root — RootLayout (title sync, devtools in DEV)"]
  RootRoute --> AuthLayout["_authenticated — auth/status gate + one Layout shell"]
  AuthLayout --> TierLayouts["_admin / _client-access — role-tier layouts"]
  TierLayouts --> Leaf["Page Content (leaf route component)"]
  AuthLayout --> Header["LayoutHeader"]
  AuthLayout --> SideNav["LayoutSideNav"]
Loading

Notification Event Flow

Application notifications flow through NotificationProvider, not through a global DOM event bus. The provider exposes toast display, event subscription, and scoped banner delivery through context-backed hooks.

graph LR
  QueryHooks["Shared Query Hooks"] --> Provider["NotificationProvider"]
  ServiceWorker["registerServiceWorker.ts"] --> Bridge["eventBridge.ts"]
  Bridge --> Provider
  Provider --> Toasts["Global Toast Notifications"]
  Provider --> ScopedHooks["useScopedNotification"]
  ScopedHooks --> Pages["WasteSearchPage / MyClientListPage"]
Loading

Routing Structure (TanStack Router, file-based)

The routing system uses @tanstack/react-router with file-based routing: every URL is a file under src/routes/, and the TanStack Router Vite plugin generates src/routeTree.gen.ts (committed, because tsc runs before vite build). The router is created once at module load in src/router.tsx as a singleton — no runtime route tree switching. Access control is structural: the pathless _authenticated layout gates auth/status, the _admin / _client-access layouts gate roles, and feature flags are checked in each route's beforeLoad.

graph LR
  subgraph "Top-level routes"
    Landing["/ — LandingPage (public-only)"]
    Dashboard["/dashboard — DashboardRedirect"]
    NoRole["/no-role — NoRolePage"]
    Unauth["/unauthorized — RoleErrorPage"]
  end

  subgraph "_authenticated — auth + status gate, one Layout shell"
    Clients["/clients — MyClientListPage"]
    Search["/search — WasteSearchPage"]
    RUDetails["/reporting-units/$ruId — ReportingUnitDetailsPage"]
    RUBlock["/reporting-units/$ruId/$blockId — ReportingUnitBlockDetailsPage"]
    Config["/configuration/** — admin tier"]
    CatchAll["$/ catch-all — NotFoundPage"]
  end

  AuthLayout["_authenticated.tsx layout"]
  AuthLayout -->|validates| UserAccessValidation["computeAuthRedirect → getUserAccessStatus"]
  RUDetails -->|loader| RULoader["reportingUnitLoader"]
  RUBlock -->|query hook| RUDetailsQuery["useReportingUnitDetailsQuery"]
  RUDetails -->|renders blocks list| RUBlocksList["ReportingUnitBlocksList"]
Loading

Diagram History

  • 2026-10-22: Replaced the HOC-guard routing diagram with the file-based router layout: top-level routes, the _authenticated gate shell, role-tier layouts, the beforeLoad flag gates, and the catch-all (file-based router migration).
  • 2026-10-01: Renamed the block details route to /reporting-units/$ruId/$blockId — ReportingUnitBlockDetailsPage (a block is a direct child of its reporting unit, mirroring the backend AttachmentController mapping) and added a ReportingUnitBlocksList render edge from ReportingUnitDetailsPage (issue #1369, decision D8).
  • 2026-10-01: Added /reporting-units/$ruId/blocks/$blockId — ReportingUnitBlockDetailsPage route with a useReportingUnitDetailsQuery query edge (no loader, no feature flag; issue #1369). useReportingUnitDetailsQuery was already listed in the shared query hooks.
  • 2026-05-19: Added /reporting-units/$ruId — ReportingUnitDetailsPage route with reportingUnitLoader loader edge.
  • 2026-04-01: Initial routing diagram published.

Feature Flag

Feature Flag Controlled The /reporting-units/$ruId route is controlled by the reporting-unit-details-enabled feature flag. The route's beforeLoad throws a router-level 404 when the flag is off, before any component renders (fail-closed: disabled means the URL is not a route). Check the current flag value and rollout status in Feature Flags.

Route lifecycle:

  1. The Vite plugin generates the route tree from src/routes/ into src/routeTree.gen.ts; router.tsx creates the router once at module load.
  2. beforeLoad hooks run first: feature-flag gates throw a router-level notFound() when their flag is off.
  3. Routes with a loader resolve data before the component mounts. reportingUnitLoader receives { context, params } and calls context.queryClient.ensureQueryData() to pre-fetch and cache reporting unit data; the page component reads the result via useLoaderData({ strict: false }). The loader converts 404/403 API responses to notFound(); flag checks stay in beforeLoad.
  4. React renders the match chain: __root (title sync) → _authenticated (auth/status gate, one <Layout> shell) → role-tier layout where the URL belongs (_admin, _client-access).
  5. DashboardRedirect on /dashboard resolves post-login redirect targets after the OAuth callback.
  6. Errors bubble to the Layout-wrapped error components on __root and _authenticated; the router-level defaultErrorComponent stays bare as the last-resort fallback so no error renders two shells.

Suspense and Lazy Loading

  • App.tsx mounts <RouterProvider router={router} /> directly, which handles async initialization internally.
  • No explicit React.lazy() is used at the component level currently — the router's autoCodeSplitting is the primary code-split boundary.
  • Manual chunks are configured in Vite's rollupOptions.manualChunks for vendor splitting.

Error Boundaries

  • Route-level: the __root and _authenticated routes supply Layout-wrapped error components (<Layout><GlobalErrorPage /></Layout>) so an error renders exactly one application shell; the router-level defaultErrorComponent is a bare GlobalErrorPage kept as the last-resort fallback.
  • GlobalErrorPage displays the error message from the error prop forwarded by the router.
  • No component-level ErrorBoundary wrappers are present; errors bubble to the route boundary.

TanStack Query Architecture

QueryClient Setup

Configured in src/config/react-query/config.ts:

Option Value Rationale
staleTime 3 hours Data changes infrequently; aggressive caching reduces server load
gcTime 3 hours Matches stale time for consistent cache lifetime
refetchOnMount false Prevents redundant fetches on re-renders
refetchOnWindowFocus false Avoids user-surprising background refreshes
retry noRetry (always false) Errors surface immediately; retry is handled at the UI layer if needed

The QueryClient is instantiated once in main.tsx and provided via <QueryClientProvider>.

Query and Mutation Patterns

  • A centralized query hook layer lives in src/config/react-query/hooks.ts, providing shared hooks: useSearchReportingUnitsQuery, useCodesQuery, useWasteSearchFilterOptionsQueries, useDistrictOptionsQuery, useForestClientsByNumbersQuery, useMyForestClientsQuery, useReportingUnitDetailsQuery, useReportingUnitExpandQuery, useClientLookupQuery.
  • Query keys are centralized in src/config/react-query/queryKeys.ts using a typed factory object (queryKeys) organized by domain, eliminating key duplication and preventing collisions.
  • Shared query input contracts are exported from the key factory (for example ReportingUnitsQueryParams) and consumed directly by hooks, preventing key-builder and hook parameter drift.
  • Components consume the shared hooks rather than calling useQuery directly.
  • Shared query hooks can emit user-facing error notifications through a notificationTarget option, keeping error-to-notification translation out of feature components.
  • Scoped query hooks deduplicate inline error notifications by tracking React Query errorUpdatedAt, preventing repeated banner emission while the same error persists across renders.
  • Services return CancelablePromise<T> which is consumed by the shared hooks.
  • Waste Search filter reference data is loaded through a single useQueries batch, while still preserving separate cache entries per code resource.
  • The full request pipeline from component to backend:
sequenceDiagram
  participant C as Component
  participant H as Shared Hook (hooks.ts)
  participant RQ as TanStack Query
  participant S as Service (e.g. SearchService)
  participant A as Axios (request.ts)
  participant MW as Middleware Stack
  participant BE as Backend API

  C->>H: useSearchReportingUnitsQuery(input)
  H->>RQ: useQuery({ queryKey, queryFn })
  RQ->>S: service.method(args)
  S->>A: doRequest(config, options)
  A->>MW: Apply request middleware
  MW->>BE: HTTP request
  BE-->>MW: HTTP response
  MW-->>A: Apply response/failure middleware
  A-->>S: Resolved data / ProblemDetails error
  S-->>RQ: CancelablePromise resolves
  RQ-->>H: data / error / isLoading
  H-->>C: data / error / isLoading
Loading
sequenceDiagram
  participant C as Component (row action)
  participant RQ as TanStack Query
  participant S as UserService
  participant A as Axios
  participant MW as Middleware Stack
  participant BE as Backend API

  C->>RQ: user clicks bookmark (optimistic UI)
  RQ->>S: API.user.setUserBookmarkedRu(ruId)
  S->>A: PUT /api/users/bookmarks/:ruId
  A->>MW: Apply middleware
  MW->>BE: HTTP request
  BE-->>MW: HTTP response
  MW-->>A: Apply response middleware
  A-->>S: resolves
  RQ-->>C: mutation settled, refetch invoked
Loading

Cache Boundaries

  • The cache is global to the app (single QueryClient).
  • No explicit cache partitioning per route or feature.
  • The long stale time (3h) means navigating between pages reuses cached data.

Invalidation Strategy

  • No explicit queryClient.invalidateQueries() calls detected in the current codebase.
  • The no-retry, long-stale-time approach relies on fresh fetches only when the user navigates to a page for the first time in a session.

Axios Architecture

Service Layer

All HTTP communication is routed through a custom HttpClient base class (defined in src/config/api/types.ts) which wraps Axios. Each service extends HttpClient:

Service File Endpoints
UserService users.service.ts Preferences CRUD
SearchService search.service.ts Reporting unit search, expand, user lookup
CodesService codes.service.ts Sampling options, districts, assessment area statuses
ForestClientService forestclient.service.ts Client lookup, search, autocomplete
ReportingUnitService reportingunit.service.ts Reporting unit details (Zod-validated)

All services are instantiated once in src/services/APIs.ts and share a single BackendApiConfig.

Interceptors / Middleware

The request pipeline supports a chain of ApiMiddleware objects, each optionally providing:

  • request(config) — pre-flight config transform
  • response(response) — post-success response transform
  • failure(error) — error normalization / offline fallback

Active middleware:

Middleware Purpose
problemDetailsMiddleware Normalizes all backend errors to RFC 7807 ProblemDetails
failureNotificationMiddleware Emits toast notifications for unscoped API failures and suppresses duplicates for scoped requests using notificationTarget / suppressFailureNotification metadata
offlineDataMiddleware Caches GET responses in IndexedDB for offline retrieval
offlineMutationMiddleware Queues POST/PUT/PATCH/DELETE in IndexedDB when offline

problemDetailsMiddleware and failureNotificationMiddleware are registered globally through BackendApiConfig.MIDDLEWARE in src/services/APIs.ts. Service methods no longer attach problemDetailsMiddleware() per request unless a call needs a truly request-specific middleware chain.

Error Normalization

All errors are converted to ProblemDetails format:

type ProblemDetails = {
  type?: string;    // URI reference ("about:blank" default)
  title: string;    // Human-readable summary
  status: number;   // HTTP status (0 for network errors)
  detail?: string;  // Occurrence-specific explanation
  instance?: string;
};

Axios error codes (ERR_NETWORK, ECONNABORTED, etc.) are mapped to user-friendly titles and descriptions.

Token Handling

  • Auth token is retrieved asynchronously from Cognito cookies via getUserTokenFromCookie().
  • BackendApiConfig.TOKEN is initialized to undefined and resolved at request time via an async resolver; no synchronous token snapshot is taken at startup.
  • Token is injected as Bearer header in every request through the getHeaders() pipeline.
  • Token refresh is handled by AuthProvider which re-fetches the session every 3 minutes.

Retry / Backoff Strategy

  • No automatic retry — noRetry always returns false.
  • No exponential backoff is implemented.
  • This is a deliberate design choice; the UI surfaces errors immediately.

TypeScript Architecture

Domain Models

Types are organized by domain and co-located with their service:

Domain Type File
Auth src/context/auth/types.ts
Search src/services/search.types.ts
Forest Client src/services/forestclient.types.ts
Pagination src/services/pagination.types.ts
API Layer src/config/api/types.ts
PWA src/config/pwa/types.ts
Preferences src/context/preference/types.ts
Reporting Unit src/services/reportingUnit.types.ts

API Response Types

  • Service methods are fully typed: CancelablePromise<PageableResponse<T>>.
  • PageableResponse<T> provides generic pagination.
  • ProblemDetails is the standard error shape.

Type Safety Guarantees

  • strict: true in all tsconfig files.
  • noUnusedLocals, noUnusedParameters, noFallthroughCasesInSwitch enabled.
  • verbatimModuleSyntax enforced for explicit import/export types.
  • Role enums and const assertions (as const) reduce string-literal drift.

Type Safety Gaps

  • Some any usage in request.ts (eslint-disabled) for Axios generic compatibility.
  • New feature flags must be added manually to the featureFlagsSchema in env.ts.

Vite Build Architecture

Plugin Graph

graph LR
  Vite --> React["@vitejs/plugin-react"]
  Vite --> TSPaths["vite-tsconfig-paths"]
  Vite --> PWA["vite-plugin-pwa (VitePWA)"]
  PWA --> Workbox["workbox (injectManifest)"]
Loading

Aliases

Alias Target
@/ ./src/

Resolved consistently in vite.config.ts, tsconfig.app.json, and eslint.config.js.

Environment Variable Handling

Two-layer approach:

  1. Build-time: Vite import.meta.env variables (prefixed VITE_).
  2. Runtime: config.js and params.js can extend window.config at deploy time.
  3. src/env.ts validates runtime string config keys before merging them into env.
  4. src/env.ts validates VITE_FEATURE_FLAGS separately and exposes typed booleans through featureFlags.

This allows container-based deployments to override env vars without rebuilding.

Runtime Config Contract

src/env.ts is the single boundary for deploy-time frontend configuration.

Input Validation Output
import.meta.env string values Filtered to string entries env
window.config string config Strict allowlist via Zod env
VITE_FEATURE_FLAGS from Vite or window.config Strict typed object via Zod featureFlags

Rules:

  • Unknown window.config keys are rejected at startup.
  • Runtime config values must match the expected type exactly.
  • Feature flags are not free-form; each flag must be declared in featureFlagsSchema.
  • Deployment overrides must extend window.config rather than writing frontend flags to localStorage.

Manual Chunks

Chunk Contents
react-vendor react, react-dom
carbon-charts @carbon/charts
carbon @carbon/*
aws aws-amplify
query @tanstack/*
router react-router*
utils axios, lodash, luxon, idb

Output directory: build/.

Build Optimizations

  • optimizeDeps.include pre-bundles large dependencies for faster cold starts.
  • chunkSizeWarningLimit: 1024 KB.
  • maximumFileSizeToCacheInBytes: 6 MB for PWA precaching.

Testing Architecture

Vitest (Unit + Component)

  • Config: Embedded in vite.config.ts under test.
  • Environment: jsdom.
  • Pattern: *.unit.test.{ts,tsx} files co-located alongside source.
  • Setup: setup-env.ts mocks AWS Amplify, APIs, fetch, and browser APIs (ResizeObserver, matchMedia).
  • Custom matchers: toContainText(), toBeEmptyDOMElement().
  • Coverage: v8 provider, 80% threshold for lines/functions/branches/statements.
  • Reporters: default + JUnit XML.

Playwright (E2E + Accessibility)

  • Config: playwright.config.ts.
  • Projects: chromium, firefox, webkit, mobile-chromium, mobile-safari, a11y-chromium.
  • Auth: Mock JWT cookies for test isolation; optional real login flow.
  • API mocking: mockApi(), mockApiResponses(), mockApiResponsesWithStub() utilities.
  • Accessibility: @axe-core/playwright with configurable impact thresholds.
  • E2E patterns: Files co-located per page (*.e2e.test.tsx).

Testing Ownership Model

Layer Framework Location
Pure logic, hooks, utils Vitest *.unit.test.ts next to source
Component render + interaction Vitest + Testing Library *.unit.test.tsx next to source
Page flows, navigation, integration Playwright *.e2e.test.tsx in src/pages/
Accessibility Playwright + Axe *.a11y.test.tsx in src/pages/

PWA Architecture (Dormant but Scaffolded)

Current Readiness

Artifact Status
vite-plugin-pwa configured Done
manifest with icons, screenshots Done
service-worker.ts with Workbox Done
registerServiceWorker.ts Done
IndexedDB offline cache middleware Done
Offline mutation queue middleware Done
Background sync handlers Placeholder
Periodic sync handlers Placeholder
Online/offline status hook Done
Feature flag for offline mode Scaffolded

PWA Lifecycle (Planned)

stateDiagram-v2
  [*] --> Online
  Online --> Offline: Network lost
  Offline --> Online: Network restored

  state Online {
    [*] --> FetchFromAPI
    FetchFromAPI --> CacheInIDB: offlineDataMiddleware
    CacheInIDB --> RenderData
  }

  state Offline {
    [*] --> ReadFromIDB
    ReadFromIDB --> RenderCachedData
    MutationQueued: offlineMutationMiddleware queues
    RenderCachedData --> MutationQueued: User action
  }

  Online --> BackgroundSync: Replay queued mutations
  BackgroundSync --> Online
Loading

Required Steps to Fully Enable

  1. Enable the offline mode feature flag in onlineStatusStore.ts.
  2. Apply offlineDataMiddleware and offlineMutationMiddleware to all service endpoints.
  3. Implement syncMutations() in syncHandlers.ts to replay queued mutations.
  4. Implement periodicRefresh() in syncHandlers.ts to keep cached data fresh.
  5. Add offline-aware UI indicators (connection status banner, sync progress).
  6. Test SW registration and update flow in production-like environments.

Recommended Caching Strategy

  • Static assets: Precache via Workbox (globPatterns).
  • API responses: Stale-while-revalidate for read-heavy endpoints (search, codes).
  • Mutations: Queue-and-replay with conflict resolution.
  • Manifest: Already configured with icons, screenshots, and display: standalone.

State Flow

graph TD
  subgraph "Server State (TanStack Query)"
    QC[QueryClient Cache]
    QC -->|staleTime: 3h| Data
  end

  subgraph "Client State (React Context)"
    Auth[AuthContext — user, login, logout]
    Layout[LayoutContext — sidenav, header panel]
    Theme[ThemeContext — Carbon theme]
    Pref[PreferenceContext — user prefs]
    Notif[NotificationContext — toasts]
    PT[PageTitleContext — page title]
  end

  subgraph "URL State"
    SP["/search URL — route-owned via validateSearch"]
    SP -->|Zod schema in searchParams.ts validates| Filters
    Filters -->|typed navigation writes| SP
  end

  subgraph "Local State"
    Comp[Component useState/useReducer]
  end

  Auth -->|token| QC
  Pref -->|theme pref| Theme
  SP -->|filter hydration| Comp
Loading

Build Pipeline

graph LR
  Source["TypeScript + SCSS + Assets"]
  Source --> TSC["tsc -b (type check)"]
  TSC --> ViteBuild["vite build"]
  ViteBuild --> Rollup["Rollup (manual chunks)"]
  Rollup --> Minify["Terser minification"]
  Minify --> PWAPlugin["vite-plugin-pwa"]
  PWAPlugin --> SW["Service Worker (injectManifest)"]
  PWAPlugin --> Manifest["manifest.webmanifest"]
  SW --> BuildDir["build/"]
  Manifest --> BuildDir
  Rollup --> BuildDir
  BuildDir --> Docker["Docker (Caddy)"]
  Docker --> OpenShift["OpenShift (deploy)"]
Loading

Deployment Architecture

Layer Technology
Web server Caddy (via Caddyfile)
Container Docker (via Dockerfile)
Orchestration OpenShift (via openshift.deploy.yml)
Runtime config public/data/config.js injected at deploy time

Waste Plus Wiki

Home

Architecture

Codes

Decisions

Knowledge Base

Development

Changes

Clone this wiki locally