Skip to content

Frontend Structure Guidelines

Paulo Gomes da Cruz Junior edited this page Oct 20, 2026 · 8 revisions

Frontend Architecture Guidelines — Waste Plus

React

Component Naming Conventions

Category Convention Example
Page components PascalCase + Page suffix WasteSearchPage
Layout components PascalCase, descriptive LayoutHeader, LayoutSideNav
Reusable components PascalCase TableResource, ActiveMultiSelect
Domain components PascalCase with domain prefix WasteSearchFilters, MyClientListing
Hook files use prefix, camelCase useBreakpoint, useNotificationEvents
Context files PascalCase + Context/Provider AuthContext, AuthProvider
Test files Source name + .unit.test.tsx or .e2e.test.tsx index.unit.test.tsx

Co-Location Rules

  • Tests: Always co-located next to the source file.
  • Types: Co-located in a types.ts file within the same directory.
  • Constants: Co-located in a constants.ts file within the same directory.
  • Utils: Co-located in a utils.ts file within the same directory.
  • Styles: Co-located SCSS modules next to the component (when needed).

Hook Patterns

  • Each custom hook lives in its own directory under src/hooks/.
  • Export the hook as the default or named export from index.tsx.
  • Hooks that manage subscriptions must clean up in a useEffect return.
  • Hooks accessing context must validate via null-check and throw if outside provider.
  • Notification event hooks must remain React-managed; do not reintroduce a DOM CustomEvent bus for application messaging.
// ✅ Correct: consumer hook with guard
export const useAuth = (): AuthContextType => {
  const context = useContext(AuthContext);
  if (!context) throw new Error('useAuth must be used within AuthProvider');
  return context;
};

Context Usage Boundaries

Context Scope When to Use
AuthContext Entire app User identity, token, login/logout
LayoutContext Below Layout Sidenav toggle, header panel state
ThemeContext Below ThemeProvider Carbon theme switching
PreferenceContext Below PreferenceProvider Persisted user preferences
NotificationContext Below NotificationProvider Toast display and notification event delivery
PageTitleContext Below PageTitleProvider Document title management

Rules:

  • Do not use context for server-fetched data — use TanStack Query.
  • Do not read context outside its provider boundary.
  • Do not store derived state in context — compute it in components.
  • Keep context state minimal; complex logic belongs in the provider.

Error Handling Patterns

  • Route-level: defaultErrorComponent on the router → GlobalErrorPage inside Layout.
  • API-level: problemDetailsMiddleware normalizes to ProblemDetails.
  • Shared hook layer: Emit user-facing query errors from src/config/react-query/hooks.ts using notificationTarget when the message belongs to a page-scoped notification flow.
  • Component-level: Consume scoped notifications with useScopedNotification; do not translate query errors into notifications inside feature components.
  • Never swallow errors silently.

Suspense and Lazy Loading Rules

  • The router singleton in routeTree.tsx is created once at module load — no useMemo or recreation on auth changes.
  • Add React.lazy() when a page grows beyond 50 KB of component code.
  • Do not nest <Suspense> boundaries without a clear loading UX.

TanStack Query

Shared Hook Layer

All useQuery and useInfiniteQuery calls for backend data must go through the shared hooks in src/config/react-query/hooks.ts. Components must not call useQuery directly.

// ✅ Correct: consume shared hook
import { useCodesQuery } from '@/config/react-query/hooks';
const { data } = useCodesQuery('districtOptions');

// ❌ Incorrect: inline useQuery in component
import { useQuery } from '@tanstack/react-query';
const { data } = useQuery({ queryKey: ['districtOptions'], queryFn: () => API.codes.getDistricts() });

Rules:

  • Add new hooks to hooks.ts when introducing a new query.
  • All query keys must be defined in src/config/react-query/queryKeys.ts using the queryKeys factory.
  • Hook function input types must reuse exported key-factory parameter types (for example ReportingUnitsQueryParams) instead of redefining local duplicates.
  • Factory keys follow the shape [domain, resource, params?] — never use plain string literals.
  • When notification routing affects cache behavior, include notificationTarget in the query key factory instead of hiding it in the query function only.
  • Pass per-callsite options (e.g. select, enabled, gcTime) as the options argument to the hook, not by re-implementing the hook inline.
  • Use notificationTarget in shared hooks when a query failure should surface as a scoped notification banner.
  • If a shared hook emits notifications from useEffect, depend on stable primitives such as errorUpdatedAt and deduplicate with a ref when batching multiple queries. Do not depend on the raw useQueries() result array.

Query Key Conventions

Use structured array keys for predictable invalidation:

// ✅ Recommended pattern
const queryKey = ['search', 'reporting-units', { filters, page, sort }];
const queryKey = ['codes', 'districts'];
const queryKey = ['forestclient', clientNumber];

Rules:

  • First element: domain/service name.
  • Second element: resource name.
  • Third element (optional): parameters object.
  • Never use string interpolation in keys.

Mutation Patterns

const mutation = useMutation({
  mutationFn: (data) => API.user.updateUserPreferences(data),
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['user', 'preferences'] });
    notification.display({ kind: 'success', title: 'Saved' });
  },
  onError: (error) => {
    notification.display({ kind: 'error', title: error.title });
  },
});

Cache Boundaries

  • Single global QueryClient — all queries share the same cache.
  • staleTime: 3 hours — data is considered fresh for 3 hours.
  • gcTime: 3 hours — garbage collection matches stale time.

Invalidation Rules

  • Invalidate after mutations that change server state.
  • Use queryClient.invalidateQueries({ queryKey: ['domain'] }) for broad invalidation.
  • Use exact key matching for targeted invalidation.
  • Do not manually set query data unless implementing optimistic updates.

Error Handling

  • retry: false — errors surface immediately.
  • Use onError callback for user-visible error handling.
  • Error shape is always ProblemDetails (guaranteed by middleware).

When to Use Query vs Mutation vs Manual Fetch

Scenario Use
Read data displayed in UI useQuery
User-triggered write operation useMutation
One-time fetch on mount (no caching) useQuery with gcTime: 0
Background data sync useQuery with refetchInterval
Fire-and-forget mutateAsync without cache update

Axios

Service Layer Conventions

  • Every backend domain gets its own service class extending HttpClient.
  • Services are instantiated once in APIs.ts and shared via import.
  • Service methods return CancelablePromise<T> for cancellation support.
  • Global middleware belongs in BackendApiConfig.MIDDLEWARE.
  • Service methods should not re-register globally configured middleware per request.
  • Include meta in request options only when it is defined; avoid emitting meta: undefined in strict request-object tests.
// ✅ Correct service method
searchReportingUnit(
  filters: ReportingUnitSearchParametersDto,
  pageable: PageableRequest<ReportingUnitSearchResultDto>,
  meta?: Record<string, unknown>,
): CancelablePromise<PageableResponse<ReportingUnitSearchResultDto>> {
  return this.doRequest(this.config, {
    method: 'GET',
    url: '/api/search/reporting-units',
    query: { ...removeEmpty(filters), ...removeEmpty(pageable) },
    ...(meta !== undefined ? { meta } : {}),
  });
}

Interceptor Patterns

The middleware chain executes in array order:

  1. problemDetailsMiddleware — must be first for error normalization.
  2. failureNotificationMiddleware — emits toast notifications for unscoped failures.
  3. offlineDataMiddleware — caches responses for offline.
  4. offlineMutationMiddleware — queues mutations for offline.

Rules:

  • Middleware must be stateless.
  • Failure handlers must either throw (to propagate) or return a value (to recover).
  • Request middleware must always return the config object.

Error Normalization

All errors conform to RFC 7807 ProblemDetails:

Field Required Description
type No URI reference (default: about:blank)
title Yes Human-readable summary
status Yes HTTP status (0 for network errors)
detail No Occurrence-specific explanation
instance No Request URL

Retry / Backoff Strategy

  • No automatic retry is configured.
  • If retry is needed in the future, implement it as middleware, not at the query level.
  • Recommended pattern: exponential backoff with jitter, max 3 retries.

TypeScript

Naming Conventions

Entity Convention Example
Types PascalCase FamLoginUser
Interfaces PascalCase (no I prefix) PageableRequest
Enums PascalCase Role, RoleType
Enum members UPPER_SNAKE_CASE Role.VIEWER
Constants UPPER_SNAKE_CASE AVAILABLE_ROLES
Functions camelCase getUserTokenFromCookie
Generics Single uppercase letter or descriptive T, PageableResponse<T>

Type vs Interface Usage

  • Use type for: union types, mapped types, utility types, function signatures.
  • Use interface for: object shapes that may be extended.
  • Prefer type over interface for DTOs (they are not extended).

API Response Typing

  • Every service method must be fully typed: CancelablePromise<ResponseType>.
  • Use PageableResponse<T> for paginated endpoints.
  • Use ProblemDetails as the error shape in catch blocks.
  • Never use any for API responses.

Domain Model Organization

  • Shared DTOs and wire schemas live in src/types/, one *.types.ts module per domain, re-exported through the index.ts barrel as @/types. The @/types barrel is the single stable import surface.
  • src/services/types.ts remains as a back-compat re-export barrel until consumers migrate. Do not add new shared types there.
  • Wire shapes follow the Zod boundary rulebook — see the Frontend Type Boundary pattern.
  • Auth types in src/context/auth/types.ts.
  • Component prop types co-located in types.ts within the component directory; runtime helpers stay component-local (e.g. TableResource/utils.ts).

Runtime Config and Feature Flags

Use src/env.ts as the only boundary for frontend runtime configuration.

Category Rule Example
Runtime config keys Declare every supported window.config string key in the Zod allowlist VITE_BACKEND_URL
Feature flags Declare every supported flag in featureFlagsSchema 'offline-mode-enabled'
Deployment format Pass flags through VITE_FEATURE_FLAGS on window.config VITE_FEATURE_FLAGS: { 'offline-mode-enabled': false }
Consumer access Read typed booleans from featureFlags, not string truthiness from env featureFlags['offline-mode-enabled']
// ✅ Correct
export const featureFlags = {
  ...getValidatedFeatureFlags(viteFeatureFlags),
  ...getValidatedFeatureFlags(runtimeFeatureFlags),
};

if (featureFlags['offline-mode-enabled']) {
  // enable offline behavior
}
// ❌ Incorrect
if (import.meta.env.VITE_FEATURE_FLAGS) {
  // serialized flag payloads are strings and are not a typed flag contract
}

Rules:

  • Reject unknown runtime config keys instead of silently accepting them.
  • Do not add ad hoc window.config keys outside env.ts.
  • Do not store deployment feature flags in localStorage.
  • When adding a new flag, update the schema, the deployment config, and tests in the same change.

Testing

Vitest Patterns

  • Use describe / it blocks with descriptive names.
  • Use @testing-library/react for component rendering.
  • Use @testing-library/user-event for interaction simulation.
  • Mock external dependencies in setup-env.ts, not per-test.
  • Use vi.fn() for function mocks, vi.spyOn() for partial mocks.
  • Prefer screen.getByRole() over getByTestId().

Playwright Patterns

  • Use Page Object pattern for complex pages.
  • Mock APIs via mockApi() / mockApiResponses() from e2e.helper.ts.
  • Use auth fixtures from auth.helper.ts for authenticated flows.
  • Tag tests: *.e2e.test.tsx for E2E, *.a11y.test.tsx for accessibility.

Mocking Strategy

Dependency Mock Location Approach
AWS Amplify setup-env.ts Global vi.mock
APIs service setup-env.ts Global vi.mock
Browser APIs (matchMedia, ResizeObserver) setup-env.ts Polyfill stubs
Backend routes (E2E) Per-test page.route() via helpers

What to Test at Each Layer

Layer What to Test Framework
Utils, pure functions Input/output, edge cases Vitest
Custom hooks State changes, side effects Vitest + renderHook
Components Rendering, interaction, accessibility Vitest + Testing Library
Pages (integration) Navigation, API integration, flow Playwright
Accessibility WCAG compliance, contrast, labels Playwright + Axe

Security

Token Handling

  • Tokens are stored in cookies (Cognito CookieStorage), not localStorage.
  • Tokens are retrieved from cookies per-request via getUserTokenFromCookie() through an async resolver.
  • BackendApiConfig.TOKEN is initialized to undefined; the async resolver is the sole setter, preventing a stale synchronous token snapshot at startup.
  • Token refresh occurs every 3 minutes in AuthProvider.
  • Never log tokens or include them in error reports.

Sanitization

  • Carbon components handle XSS for rendered text.
  • Do not use dangerouslySetInnerHTML.
  • Sanitize user input before sending to APIs.

Safe API Patterns

  • Always use removeEmpty() to strip falsy values from query parameters.
  • Use the query option (not string concatenation) for query parameters.
  • Use path templates ({param}) with encodeURI for URL path segments.
  • Never interpolate user input directly into URLs.

Runtime Configuration

  • Treat window.config as untrusted input and validate it before use.
  • Keep runtime config parsing centralized in src/env.ts.
  • Fail fast on malformed VITE_FEATURE_FLAGS JSON or unexpected flag names.
  • Prefer typed feature flag consumers over string comparisons in components and hooks.

PWA Security Considerations

  • Service workers run on a separate thread; ensure they do not expose tokens.
  • IndexedDB data should be scoped to the authenticated user.
  • Queued mutations must include auth context for replay.
  • Use HTTPS only (enforced by Caddy/OpenShift).

Performance

Code Splitting

Manual chunks in vite.config.ts split vendor code:

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

Memoization Rules

  • Use useMemo for expensive computations that depend on stable inputs.
  • Use useCallback for functions passed as props to memoized children.
  • Do not memoize everything — only optimize measured bottlenecks.
  • The router singleton in routeTree.tsx is a module-level constant and does not need memoization.

Vite Optimization Hints

  • optimizeDeps.include pre-bundles heavy dependencies for faster cold starts.
  • chunkSizeWarningLimit: 1024 flags chunks over 1 MB.
  • server.hmr.overlay: false avoids intrusive error overlays during development.
  • Font and image preloading in index.html reduces first-paint latency.

Accessibility

Required Patterns

  • Use Carbon components as the base — they include ARIA attributes by default.
  • Every interactive element must have an accessible name (aria-label or visible label).
  • Every page must have a single <h1> (provided by PageTitle).
  • Use semantic HTML: <nav>, <main>, <header>, <footer>.
  • Color must not be the sole means of conveying information.
  • Focus management must be handled on route transitions.

Testing Guidelines

  • Run test:a11y (Playwright + Axe) on all pages.
  • Axe violations at critical and serious impact levels must be fixed.
  • Add *.a11y.test.tsx files for each page.
  • Test keyboard navigation for all interactive flows.
  • Verify screen reader output for key user journeys.

Waste Plus Wiki

Home

Architecture

Codes

Decisions

Knowledge Base

Development

Changes

Clone this wiki locally