Skip to content

Latest commit

 

History

History
194 lines (162 loc) · 12.2 KB

File metadata and controls

194 lines (162 loc) · 12.2 KB

Coding Standards

Conventions enforced across the SaaS Business App frontend. These rules are grounded in the actual tooling configuration (tsconfig.json, eslint.config.js, .prettierrc, commitlint.config.js) and the patterns already present in the codebase.

1. TypeScript

Configured in tsconfig.json and tsconfig.app.json:

  • strict: true (strict null checks, strict property initialization, strict function types)
  • noImplicitOverride: true
  • noImplicitReturns: true
  • noFallthroughCasesInSwitch: true
  • noUnusedLocals: true and noUnusedParameters: true
  • noPropertyAccessFromIndexSignature: true
  • isolatedModules: true, target: ES2022, module: preserve
  • Angular compiler: strictInjectionParameters, strictInputAccessModifiers, strictTemplates (all true)
  • Path aliases only (@core, @domain, @infrastructure, @features, @ui, @layout, @config, @env); no relative imports that cross feature boundaries.

any is banned

@typescript-eslint/no-explicit-any is set to error for src/**/*.ts. Use unknown and narrow, or define a proper type. The only relaxation is in spec files (*.spec.ts/*.test.ts), which also relax no-non-null-assertion and no-unsafe-assignment.

Additional ESLint rules (eslint.config.js)

Rule Level
@typescript-eslint/no-unused-vars error (argsIgnorePattern: '^_')
@typescript-eslint/prefer-nullish-coalescing error
@typescript-eslint/prefer-optional-chain error
@typescript-eslint/consistent-type-definitions error, interface
@typescript-eslint/no-non-null-assertion warn
no-console warn (allow warn, error)
prefer-const / no-var / eqeqeq (always) error

Use interface for object shapes (not type aliases). Use type for unions and aliases.

2. Naming conventions

Element Convention Example Reference
Feature directory kebab-case features/user-management/ (current features are single words: users, orders)
Component class XComponent ButtonComponent, PrivateLayoutComponent ui/atoms/button/button.component.ts
Page class XPage LoginPage, UserListPage features/authentication/presentation/pages/login/login.page.ts
Service class XService AuthApiService, StorageService
Store (facade) XStore AuthStore, UsersStore features/users/application/facade/users.store.ts
Repository interface IXRepository IAuthRepository, IUserRepository domain/repository/iauth.repository.ts
Repository implementation XApiService AuthApiService, UsersApiService infrastructure/http/auth-api.service.ts
DTO XDto / CreateXDto LoginRequestDto, UserDto application/dto/
Mapper XMapper AuthMapper, UserMapper application/mappers/
Signal (local) camelCase sidebarCollapsed private-layout.component.ts
Observable suffix $ (RxJS streams only)
Enum PascalCase + members Role.ADMIN domain/enums/role.enum.ts
File kebab-case auth-refresh.service.ts, global-error.handler.ts
Selector app-<kebab> or attribute app-input, button[app-button]

DTO fields use snake_case (backend wire format). Entity fields use camelCase. The mapper is the only code that translates between the two.

3. Architecture rules

  1. Feature isolation. Features never import another feature's infrastructure/ or presentation/. They may import another feature's store facade only when genuinely required, and may always import shared domain/ entities.
  2. Dependency direction. presentation -> application -> domain <- infrastructure. The domain/ layer has no Angular or IO imports (it is pure TypeScript).
  3. No cross-feature infrastructure/presentation imports. Shared HTTP goes through src/app/infrastructure/http/api.service.ts; shared UI lives in src/app/ui/.
  4. Repository contracts. Feature I/O is declared as an IXRepository interface in domain/repository/ and implemented in infrastructure/http/. The store depends on the implementation (injected via inject(XApiService)); swap implementations (e.g. mock) by changing the provider binding.
  5. Mappers are injectable. @Injectable({ providedIn: 'root' }) classes that own the DTO-to-entity translation. No mapping logic in stores, pages, or API services beyond the if (result.isSuccess) return mapper.toX(result.data) unwrap.

4. State management

  • One SignalStore per feature, { providedIn: 'root' }, colocated in application/facade/<feature>.store.ts.
  • Async work uses rxMethod (from @ngrx/signals/rxjs-interop) so components call store.load(...) without manual subscriptions.
  • Split withMethods blocks when a method calls another store method: the callee must be defined in an earlier block than the caller (the store argument only sees previously registered methods). rxMethods without cross-dependencies may share a block with independent sync helpers. See users.store.ts (two blocks) vs dashboard.store.ts (one block).
  • patchState for immutable state updates; never mutate store state directly.
  • withComputed for derived state (e.g. AuthStore.fullName, isAdmin).
  • Local UI state uses signal() / computed() inside components (e.g. PrivateLayoutComponent.sidebarCollapsed). Do not promote local state to a store prematurely.
  • RxJS only for HTTP streams. Avoid BehaviorSubject for application state; prefer signals.

5. API layer

  • All HTTP through ApiService (src/app/infrastructure/http/api.service.ts), which returns Observable<Result<T>>. Never inject HttpClient outside infrastructure/.
  • Result<T> is the error channel for expected failures. Branch on result.isSuccess / result.isFailure; do not use try/catch around HTTP. Repository implementations unwrap by throw-ing result.error on failure so the store's catchError can react; either style is acceptable, but pick one per call site.
  • Config via tokens, not magic values: API_CONFIG (baseUrl, timeout, retries), LOGGER, AUTH_STORAGE, APP_NAME, APP_VERSION, API_BASE_URL.
  • DTOs are snake_case, entities are camelCase with rich types (Date, enums). Conversion happens only in mappers.
  • Typed params: use HttpParams for query strings (see users-api.service.ts).
  • No any in DTOs or entities. Backend string unions are narrowed with as Entity['field'] inside the mapper only.

6. Components

  • Standalone (standalone: true); no NgModules.
  • OnPush change detection on every component (changeDetection: ChangeDetectionStrategy.OnPush).
  • Signal inputs: input() for one-way, model() for two-way. No @Input()/@Output() decorators. See ButtonComponent (input<ButtonVariant>) and InputComponent (model<string>()).
  • Control flow: @if / @for / @switch in templates. Do not use *ngIf, *ngFor, or *ngSwitch.
  • Inject with inject(), not constructor injection, in stores and interceptors; constructor injection is acceptable in components.
  • Colocate styles inline or as styleUrl; keep component styles scoped to the component. Use :host / :host-context(...) for variant styling driven by host classes.
  • Forms: InputComponent implements ControlValueAccessor (NG_VALUE_ACCESSOR) so it composes with both template-driven (ngModel) and reactive forms. Pages may use either; keep form state in signals where practical.

7. Styling

  • SCSS + Tailwind v4. Tailwind is loaded via PostCSS (.postcssrc.json) and src/tailwind.css; global styles in src/styles.scss.
  • Design tokens are CSS custom properties defined in styles.scss (:root light, [data-theme='dark'] dark): color, typography, spacing, radius, shadow, motion.
  • No hardcoded colors, sizes, spacing, or radii in components. Reference tokens: var(--color-primary), var(--spacing-4), var(--radius-lg), etc.
  • Dark mode is driven by ThemeService, which toggles data-theme on <html>; tokens re-theme automatically. Do not write component-specific dark-mode overrides; add/override tokens in styles.scss.
  • Prettier (.prettierrc): printWidth: 100, singleQuote: true, Angular parser for .html. Format on commit via lint-staged.

8. Accessibility

  • Semantic HTML first: native <button>, <label for>, <nav>, <main>, <router-outlet> within layout shells.
  • ARIA where needed: aria-busy / aria-disabled on loading buttons (ButtonComponent), aria-invalid / aria-describedby and role="alert" on errors (InputComponent).
  • Keyboard navigation: visible :focus-visible ring (global in styles.scss); interactive elements must be focusable and operable by keyboard.
  • Color contrast: target WCAG AA. Token palettes are tuned for light and dark themes.
  • Respect prefers-color-scheme (done by ThemeService.getInitialTheme).

9. Git conventions

Conventional Commits, enforced by commitlint (commitlint.config.js) on the commit-msg hook (.husky/commit-msg).

Allowed types:

feat  fix  docs  style  refactor  perf  test  build  ci  chore  revert

Format: type(scope): subject (subject lowercase, imperative, <= 72 chars).

The pre-commit hook (.husky/pre-commit) runs lint-staged: Prettier --write on *.{ts,html,scss,css,json,md} and eslint --fix on *.ts. Only staged files are formatted.

10. Code quality

  • SOLID / DRY / KISS / YAGNI. Do not add abstractions for hypothetical needs.
  • No dead code. noUnusedLocals/noUnusedParameters are compiler errors; unused parameters that are positional must be prefixed with _ (argsIgnorePattern: '^_').
  • No magic strings or numbers. Extract to enums (domain/enums/), constants, or config tokens. Route paths, storage keys, and API paths are the main exceptions and should still be grouped.
  • No console.* outside the logger implementation (no-console warns; use LOGGER).
  • No non-null assertions when avoidable (rule is warn); prefer optional chaining and nullish coalescing (both error).
  • Prefer interface for object shapes; type for unions/aliases/mapped types.
  • Barrel files (index.ts) are used for core/, core/interceptors/, domain/, ui/atoms|molecules|organisms, and config/; re-export public APIs only.