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.
Configured in tsconfig.json and tsconfig.app.json:
strict: true(strict null checks, strict property initialization, strict function types)noImplicitOverride: truenoImplicitReturns: truenoFallthroughCasesInSwitch: truenoUnusedLocals: trueandnoUnusedParameters: truenoPropertyAccessFromIndexSignature: trueisolatedModules: true,target: ES2022,module: preserve- Angular compiler:
strictInjectionParameters,strictInputAccessModifiers,strictTemplates(alltrue) - Path aliases only (
@core,@domain,@infrastructure,@features,@ui,@layout,@config,@env); no relative imports that cross feature boundaries.
@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.
| 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.
| 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.
- Feature isolation. Features never import another feature's
infrastructure/orpresentation/. They may import another feature's store facade only when genuinely required, and may always import shareddomain/entities. - Dependency direction.
presentation -> application -> domain <- infrastructure. Thedomain/layer has no Angular or IO imports (it is pure TypeScript). - No cross-feature infrastructure/presentation imports. Shared HTTP goes through
src/app/infrastructure/http/api.service.ts; shared UI lives insrc/app/ui/. - Repository contracts. Feature I/O is declared as an
IXRepositoryinterface indomain/repository/and implemented ininfrastructure/http/. The store depends on the implementation (injected viainject(XApiService)); swap implementations (e.g. mock) by changing the provider binding. - Mappers are injectable.
@Injectable({ providedIn: 'root' })classes that own the DTO-to-entity translation. No mapping logic in stores, pages, or API services beyond theif (result.isSuccess) return mapper.toX(result.data)unwrap.
- One SignalStore per feature,
{ providedIn: 'root' }, colocated inapplication/facade/<feature>.store.ts. - Async work uses
rxMethod(from@ngrx/signals/rxjs-interop) so components callstore.load(...)without manual subscriptions. - Split
withMethodsblocks when a method calls another store method: the callee must be defined in an earlier block than the caller (thestoreargument only sees previously registered methods).rxMethods without cross-dependencies may share a block with independent sync helpers. Seeusers.store.ts(two blocks) vsdashboard.store.ts(one block). patchStatefor immutable state updates; never mutate store state directly.withComputedfor 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
BehaviorSubjectfor application state; prefer signals.
- All HTTP through
ApiService(src/app/infrastructure/http/api.service.ts), which returnsObservable<Result<T>>. Never injectHttpClientoutsideinfrastructure/. Result<T>is the error channel for expected failures. Branch onresult.isSuccess/result.isFailure; do not usetry/catcharound HTTP. Repository implementations unwrap bythrow-ingresult.erroron failure so the store'scatchErrorcan 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 arecamelCasewith rich types (Date, enums). Conversion happens only in mappers. - Typed params: use
HttpParamsfor query strings (seeusers-api.service.ts). - No
anyin DTOs or entities. Backend string unions are narrowed withas Entity['field']inside the mapper only.
- 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. SeeButtonComponent(input<ButtonVariant>) andInputComponent(model<string>()). - Control flow:
@if/@for/@switchin 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:
InputComponentimplementsControlValueAccessor(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.
- SCSS + Tailwind v4. Tailwind is loaded via PostCSS (
.postcssrc.json) andsrc/tailwind.css; global styles insrc/styles.scss. - Design tokens are CSS custom properties defined in
styles.scss(:rootlight,[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 togglesdata-themeon<html>; tokens re-theme automatically. Do not write component-specific dark-mode overrides; add/override tokens instyles.scss. - Prettier (
.prettierrc):printWidth: 100,singleQuote: true, Angular parser for.html. Format on commit via lint-staged.
- Semantic HTML first: native
<button>,<label for>,<nav>,<main>,<router-outlet>within layout shells. - ARIA where needed:
aria-busy/aria-disabledon loading buttons (ButtonComponent),aria-invalid/aria-describedbyandrole="alert"on errors (InputComponent). - Keyboard navigation: visible
:focus-visiblering (global instyles.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 byThemeService.getInitialTheme).
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.
- SOLID / DRY / KISS / YAGNI. Do not add abstractions for hypothetical needs.
- No dead code.
noUnusedLocals/noUnusedParametersare 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-consolewarns; useLOGGER). - No non-null assertions when avoidable (rule is
warn); prefer optional chaining and nullish coalescing (botherror). - Prefer
interfacefor object shapes;typefor unions/aliases/mapped types. - Barrel files (
index.ts) are used forcore/,core/interceptors/,domain/,ui/atoms|molecules|organisms, andconfig/; re-export public APIs only.