Angular Frontend Standard

*Status:* v1.1 authoritative · Owner: Frontend team (CTO sign-off) · Last reviewed: 2026-07-03 · Review cadence: quarterly

The standard for FT frontend teams (Angular / TypeScript; reference repo angular-ui).

It is self-contained — a frontend engineer needs only this page (plus the shared FE↔BE contract).

Backend engineers have their own Java standard.

This page is the visual form of the authoritative ft-angular-standards skill; on conflict with any other Angular standard, this wins.

Two standards, two teams, one platform. They diverge only where the stacks genuinely differ (e.g. the I-prefix on interfaces) and they share exactly one thing: the wire contract.

C1 · Folders = the catalog of the app ⭐

Same idea as the backend’s packages: the folder tree is how you catalog and find code. A new file goes into its feature folder, named by its role, and is exposed through a barrel (index.ts / public-api.ts). Never dump a file at the app root, and never inline helper logic in a component.

A feature folder, catalogued by role
Figure 1. A feature folder, catalogued by role
✅ Do — feature first, role second ❌ Don’t
app/device/
  components/device-list.component.ts
  services/device.service.ts
  store/device.effects.ts
  models/device.model.ts
  device-list.helper.ts   // pure helpers, co-located
  index.ts                // barrel
src/app/DeviceList.ts     // dumped at root, no role

Workspace: a shared ft-common library + apps. Import across features only through the barrel. Selector prefixes: ft-common (library) / app (apps).

C2 · Naming — role suffix, no Hungarian

Rule Angular (TS)

Role suffix

.component.ts / .service.ts / .guard.ts / .pipe.ts / .directive.ts / .effects.ts / *.reducer.ts

Enums

end with …​Enum

Interfaces

keep the I-prefix (IDevice, ICollection<T>) — FE norm

Variables/methods

camelCase; predicate booleans (isLoading, hasError); verb methods (loadDevices); handlers on<Event>; observables end $

Forbidden

❌ Hungarian notation

"Hungarian notation" = baking a variable’s type or scope into its name via a prefix (strName, nCount, bEnabled, arrItems, _$obs). Name by meaning, not type — name, count, enabled, items. The one allowed prefix is I on interfaces (IDevice); it is a deliberate FE convention, not Hungarian on variables.

Class suffixes are enforced by @angular-eslint. The I-prefix is the one intentional divergence from the Java standard (which drops it).
Both the I-prefix and the …​Enum suffix are deliberate FT conventions for greppability and uniform cataloguing — not a claim that either is the official Angular/TS idiom (the Angular style guide suffixes neither). Where idiom and convention diverge, the convention wins on these pages.

C3 · Immutability by default

✅ readonly model ❌ in-place mutation
export interface IWriteResult {
  readonly written: number;
  readonly at: string;
}
state.devices.push(d);   // mutating NgRx state
model.name = 'x';        // mutating a shared model

NgRx state is updated by spread; use as const for literals. Mutable types only where Angular requires them (reactive form models).

C4 · Constructor injection · OnPush

Inject through the constructor (or inject(), consistently with the repo). Components are ChangeDetectionStrategy.OnPush; a BaseComponent provides destroy$ + the takeUntil pattern; unwrap observables with the async pipe.

C5 · Logging ⭐

Log through the app logger, never raw console. Include the device context you already have as key=value so a frontend log greps like a backend one.

✅ Do ❌ Don’t
this.logger.info('device write requested',
  { deviceId: serial });
console.log('write for ' + serial);
Levels: error for failed ops surfaced to the user, warn for degraded/retried, info for notable user actions, debug for dev detail (silenced in prod). ❌ No per-render / per-keystroke logging; ❌ no stray console.log in committed code — use the app logger.

C6 · Architecture

Area Standard

Components

NgModule (not standalone), OnPush, smart/dumb split, async pipe, external templates + SCSS

State

RxJS (no signals yet) + NgRx; errors surface through effects → toastr

Structure

ft-common lib + apps; feature folders; barrels; selectors ft-common / app

NgModule (not standalone), RxJS-only (no signals), and the BaseComponent + destroy$/takeUntil pattern are a deliberate choice tied to the current Angular major — not the framework’s current default (standalone, signals, and takeUntilDestroyed/DestroyRef are). Revisit on the next major upgrade; until then this is the norm, so don’t introduce the newer patterns piecemeal.

C7 · Style — current permissive norm (keep, do NOT harden)

Config Setting (keep as-is)

.prettierrc

printWidth 120, single quotes, semis, trailingComma es5, arrowParens: avoid

.editorconfig

2 spaces

.eslintrc.json

the existing rules

Do not tighten tsconfig / eslint strictness as part of unrelated work — the permissive norm is deliberate.

C8 · Tests

  • Specs next to the code (*.spec.ts); name behaviourally (should … when …); group related cases.

  • Test components / services / effects with the Angular testing utilities; mock HTTP at the ApiService boundary.

  • Coverage: new/changed code ≥ 80% (diff coverage); no global gate. Cover behaviour (reducers as pure functions, effects with a mocked actions stream), not lines.

  • Spec first: a new feature (architecture-led) needs an approved SDD before implementation; a bug fix is TDD-led (failing test first).

C9 · Constants & enums

  • ❌ No magic numbers/strings — name them; group in a *.const.ts or an as const object. Prefer a string enum (…​Enum) or a const union over scattered literals.

  • Route paths, NgRx action types, storage keys, error codes — single source of truth as constants, never re-typed inline.

  • Build/config values live in Angular environment.*, not hard-coded constants.

C10 · Caching & client state

  • NgRx is the client cache — read from selectors; don’t re-fetch what’s already in the store. An HTTP call the store can answer is a bug.

  • Cache HTTP responses deliberately (interceptor / service) with a clear invalidation trigger; ❌ no unbounded caches.

  • Don’t duplicate server state in component fields — derive from selectors via the async pipe.

C11 · External libraries

  • Prefer Angular / RxJS / the existing stack first; ❌ no second library for a job an existing one does (no extra date / HTTP / state libs). Mind bundle size.

  • A new dependency needs approval; pin exact versions (lockfile). Respect licenses; avoid abandoned packages.

  • npm audit is part of "done" when deps change — upgrade when safe, flag HIGH/CRITICAL you can’t bump; ❌ never --force.

C12 · Component & code discipline — code that stays readable ⭐

Review norms, not CI gates: exceeding one needs a stated reason in the PR, not silence.

Threshold Rule

Component class ≤ 200 lines

larger → split (child components, co-located *.helper.ts, logic into services/effects)

7 `@Input`s

more → group into a model object

Method ≤ 40 lines · nesting ≤ 3

guard clauses / early return — same norm as the backend

  • Smart/dumb split enforced (C6): dumb components take inputs and emit outputs — no store access, no HTTP.

  • Rule of Three: 2 duplicates → leave + mark // DUPE:; the 3rd → extract (shared helper / ft-common). Look-alike blocks changing for different reasons are not duplicates.

  • YAGNI applies to capabilities, never to quality — no speculative `@Input`s or generic wrappers without a second consumer; never an excuse to skip tests/error handling.

  • Comments say WHY, not what; ❌ no commented-out code, no dead exports in barrels.

  • Simplicity first (KISS): the simplest code that passes the tests and reveals intent — arrow functions and small pure helpers over stateless classes; the simplest RxJS chain that works (2–3 well-named operators beat a clever 7-operator one-liner); ❌ no abstractions/patterns (wrapper services, generic base classes) without a present second consumer; ❌ no clever one-liners that need a comment to decode; no premature optimization — measure first.

C13 · RxJS discipline

  • No nested subscribe — compose with a flattening operator, chosen by semantics:

Operator

When

switchMap

only the latest matters — typeahead, route params, reload

exhaustMap

ignore re-triggers while busy — form submit, login click (double-submit guard)

concatMap

every event matters, in order — queued writes

mergeMap

independent parallel work (rare; justify the concurrency)

  • catchError goes INSIDE the inner stream of an effect — on the flattened HTTP observable, returning a failure action. On the outer stream it kills the effect after the first error (it silently stops dispatching forever).

  • ❌ No manual subscribe in components where the async pipe can unwrap (C4/C6); a manual subscription always pairs with takeUntil(destroy$).

  • Multicast expensive shared streams deliberately — shareReplay({ bufferSize: 1, refCount: true }) — instead of re-triggering the source per subscriber.

C14 · Templates & rendering performance

  • trackBy on every *ngFor over changing data — without it OnPush re-creates the DOM per emission.

  • No function calls in template expressions ({{ compute(x) }}) — they run every change-detection cycle; precompute in the component/selector or use a pure pipe. Derive view state in memoized NgRx selectors, not getters.

  • Lazy-load feature routes (loadChildren) — a new feature module ships lazy by default.

  • Heavy lists: CDK virtual scroll before pagination hacks; images sized/lazy.

C15 · Security baseline (frontend)

Violations here are blocking review findings, not style notes.
  • No [innerHTML] / bypassSecurityTrust with user- or device-supplied content* — Angular sanitizes interpolation by default; don’t opt out. A genuine rich-HTML need goes through the sanitizer and a review.

  • Secrets never reach the bundle: environment.*.ts holds endpoints and flags — ❌ no API keys/credentials (everything in the bundle is public). ❌ Never log tokens/credentials via the app logger (C5).

  • ❌ No eval / new Function / dynamic <script> injection.

  • External links with target="_blank" carry rel="noopener noreferrer".

  • The auth/session storage scheme follows the repo’s established mechanism — moving tokens between localStorage/cookie/memory is a CTO-level architecture decision, not a drive-by "fix".

✅ Review checklist

  • New file in a feature folder by role, exported via the barrel — not the app root

  • readonly / immutable models & NgRx state (spread, no in-place mutation)

  • Constructor injection; components OnPush; destroy$ + takeUntil

  • FE logs via the app logger as key=value (e.g. deviceId); right level, no per-render noise / stray console.log

  • Naming: .component.ts/.service.ts…; interfaces I-prefixed; camelCase vars, verb methods; enums end …​Enum

  • No magic numbers/strings; constants/as const/enum; reads from NgRx store (no needless re-fetch)

  • New code ≥ 80% diff coverage; new dependency justified + pinned + npm audit-clean; no duplicate-purpose lib

  • Style left at the current permissive norm

  • FE↔BE contract matches the backend exactly

  • Size discipline: component ≤ 200 lines / ≤ 7 inputs; methods ≤ 40 lines, nesting ≤ 3; dumb components have no store/HTTP

  • Simplicity: pure helpers over stateless classes; simplest RxJS chain that works; no abstractions without a second consumer; no clever one-liners

  • RxJS: no nested subscribes; right flattening operator (exhaustMap submits / switchMap latest-wins / concatMap ordered); catchError inside the effect’s inner stream

  • Templates: trackBy on every ngFor; *no function calls in template expressions; new features lazy-loaded

  • Security: no [innerHTML]/bypassSecurityTrust* on user content; no secrets in environment.*/bundle; no eval; rel="noopener" on _blank

Next → the shared FE↔BE contract · the Java standard.