A production-ready enterprise SaaS application built with Angular 22.0.7, combining Domain-Driven Design (DDD), Vertical Slice Architecture, Clean Architecture, and Atomic Design UI methodology. State management via NgRx SignalStore with Angular Signals, styled with Tailwind CSS v4 and SCSS.
| Category | Technology |
|---|---|
| Framework | Angular 22.0.7 (Standalone Components, Signals, esbuild) |
| Language | TypeScript 6.0 (strict) |
| State Management | NgRx SignalStore 21.1.1 (signals), RxJS (HTTP streams only) |
| UI | Atomic Design (atoms/molecules/organisms), Angular Material 22 |
| Styling | Tailwind CSS v4, SCSS, CSS Custom Properties (design tokens) |
| Build | @angular/build:application (esbuild, PostCSS) |
| Testing | Vitest (unit), Playwright (E2E) |
| Linting | ESLint flat config, Prettier |
| Git Hooks | Husky, commitlint, lint-staged |
| CI/CD | GitHub Actions, Docker (multi-stage, Nginx) |
| Runtime | Node 24.18.0, Nginx |
src/
├── app/
│ ├── core/ # Cross-cutting services (theme, navigation)
│ ├── domain/ # Shared domain primitives (BaseEntity, ValueObject, enums)
│ ├── infrastructure/ # Shared infrastructure (HTTP, storage, logging)
│ ├── config/ # Environment config, feature flags
│ ├── layout/ # App shells (public, private), sidebar, topbar
│ ├── ui/ # Atomic Design system (atoms → molecules → organisms)
│ └── features/ # Vertical slices
│ └── <feature>/
│ ├── domain/ # Entities, repository interfaces
│ │ ├── entities/
│ │ └── repository/
│ ├── application/ # Use cases, facades/stores, DTOs, mappers
│ │ ├── facade/
│ │ ├── dto/
│ │ └── mappers/
│ ├── infrastructure/ # API implementations
│ │ └── http/
│ └── presentation/ # Pages, components
│ └── pages/
Each feature is a self-contained vertical slice with its own domain, application, infrastructure, and presentation layers. Features communicate only through shared domain entities or the store.
- Global/feature state: NgRx SignalStore with
rxMethodfor async operations - Local/UI state: Angular Signals (
signal(),computed()) - HTTP: RxJS Observable streams → wrapped in
Result<T>discriminated union
SignalStores follow a consistent pattern:
signalStore(
{ providedIn: 'root' },
withState(initialState),
withMethods((store, api = inject(ApiService)) => ({
load: rxMethod<void>(pipe(
tap(() => patchState(store, { isLoading: true })),
switchMap(() => api.get().pipe(tap(…), catchError(…))),
)),
})),
)Note:
rxMethodmethods and regular methods that callstore.method()must live in separatewithMethodsblocks due to type inference constraints.
All HTTP calls go through ApiService which wraps responses in a Result<T> discriminated union:
type Result<T> = Success<T> | Failure;
interface Success<T> {
data: T;
isSuccess: true;
error: null;
}
interface Failure {
data: null;
isSuccess: false;
error: Error;
}Repository implementations map from API DTOs to domain entities via injectable Mapper classes. The API is mock-ready — each feature's repository interface can be swapped with a mock implementation.
ui/
├── atoms/ # Single-responsibility components
│ ├── button.component.ts
│ ├── input.component.ts (ControlValueAccessor)
│ ├── badge.component.ts
│ ├── avatar.component.ts
│ └── spinner.component.ts
├── molecules/ # Composite components (2-3 atoms)
│ ├── card.component.ts
│ └── toast.component.ts
└── organisms/ # Complex, domain-agnostic widgets
├── data-table.component.ts
├── dialog.component.ts
├── form-field.component.ts
├── empty-state.component.ts
└── error-state.component.ts
Design tokens are defined as CSS custom properties in styles.scss and shared across all components.
All routes are lazy-loaded with standalone components.
| Route | Feature | Status |
|---|---|---|
/auth/login |
Authentication | Done |
/auth/register |
Registration | Done |
/app/dashboard |
Dashboard | Done |
/app/users |
User CRUD | Done |
/app/products |
Products | Done |
/app/orders |
Orders | Done |
/app/notifications |
Notifications | Done |
/app/profile |
Profile | Done |
/403 |
Forbidden | Done |
/** |
Not Found | Done |
- JWT + refresh token flow
AuthStore(SignalStore) manages session stateAuthInterceptorattaches JWT fromStorageServiceAuthGuardprotects private routes
- Full CRUD with pagination, search, DataTable
UsersStorewithload,setPage,setSearch,deleteUser- User list with sortable columns and pagination controls
- Stats cards (total users, revenue, orders, growth, etc.)
- Loading skeleton states and error handling
DashboardStorewith singleloadrxMethod
- Notification list with unread indicators and mark-as-read
- Profile edit form with save
- List views with DataTable and pagination
- Products: pageable with search
- Orders: pageable list
- Node.js >= 22.18.0 (v22+ required for Angular 22)
- npm >= 11
- Angular CLI (optional,
npx ngworks)
npm cinpm start
# or
ng serveNavigate to http://localhost:4200/. The app auto-reloads on source changes.
| Command | Description |
|---|---|
npm start |
Start dev server |
npm run build |
Production build |
npm run watch |
Build in watch mode (dev) |
npm test |
Run unit tests (Vitest) |
npm run lint |
ESLint check |
npm run format |
Format with Prettier |
npm run format:check |
Check Prettier formatting |
npm run buildBuild artifacts go to dist/saas-business-app/browser/. The build uses esbuild for fast bundling and tree-shaking.
docker build -t saas-business-app .docker run -p 80:80 saas-business-appdocker compose upThe Dockerfile uses a multi-stage build:
- Build stage:
node:24-alpine— installs deps, runs production build - Serve stage:
nginx:alpine— serves the built artifacts with Nginx
Nginx is configured with SPA fallback (try_files $uri /index.html) and an /api/ reverse proxy.
Push to main or open a PR to trigger:
- Lint — ESLint on the full codebase
- Build — Production build, artifacts uploaded
Workflow: .github/workflows/ci.yml
.
├── .dockerignore
├── .github/
│ └── workflows/
│ └── ci.yml # CI pipeline
├── .husky/
│ ├── commit-msg # commitlint
│ └── pre-commit # lint-staged
├── .postcssrc.json # PostCSS with @tailwindcss/postcss
├── .prettierrc # Prettier config
├── Dockerfile # Multi-stage Docker build
├── commitlint.config.js
├── docker-compose.yml
├── eslint.config.js # ESLint flat config
├── nginx.conf # Nginx SPA config
├── package.json
├── tsconfig.json # Strict TS, path aliases
└── src/
└── app/
├── app.component.ts
├── app.config.ts # DI providers (interceptors, configs, tokens)
├── app.routes.ts # Lazy-loaded route tree
├── core/
│ ├── services/
│ │ ├── navigation.service.ts
│ │ └── theme.service.ts
│ ├── interceptors/
│ │ ├── auth.interceptor.ts
│ │ ├── error.interceptor.ts
│ │ └── logging.interceptor.ts
│ └── guards/
│ └── auth.guard.ts
├── domain/
│ ├── entities/
│ │ └── base.entity.ts
│ ├── enums/
│ │ ├── permission.enum.ts
│ │ ├── role.enum.ts
│ │ ├── status.enum.ts
│ │ └── theme.enum.ts
│ └── exceptions/
│ ├── domain.exception.ts
│ ├── not-found.exception.ts
│ ├── unauthorized.exception.ts
│ └── validation.exception.ts
├── infrastructure/
│ ├── http/
│ │ ├── api.service.ts # Base HTTP with Result<T>
│ │ ├── api-config.ts # API_CONFIG token
│ │ └── result.ts # Result discriminated union
│ ├── logger/
│ │ └── logger.service.ts # LOGGER token + console impl
│ └── storage/
│ └── storage.service.ts # localStorage/ssr-safe storage
├── config/
│ ├── environment.ts
│ └── feature-flags.ts
├── layout/
│ ├── pages/
│ │ ├── forbidden/
│ │ │ └── forbidden.component.ts
│ │ └── not-found/
│ │ └── not-found.component.ts
│ ├── private-layout/
│ │ ├── private-layout.component.ts
│ │ ├── sidebar/
│ │ │ └── sidebar.component.ts
│ │ └── topbar/
│ │ └── topbar.component.ts
│ ├── public-layout/
│ │ └── public-layout.component.ts
│ └── breadcrumb/
│ └── breadcrumb.component.ts
├── ui/
│ ├── atoms/
│ │ ├── avatar/
│ │ │ └── avatar.component.ts
│ │ ├── badge/
│ │ │ └── badge.component.ts
│ │ ├── button/
│ │ │ └── button.component.ts
│ │ ├── input/
│ │ │ └── input.component.ts
│ │ └── spinner/
│ │ └── spinner.component.ts
│ ├── molecules/
│ │ ├── card/
│ │ │ └── card.component.ts
│ │ └── toast/
│ │ └── toast.component.ts
│ └── organisms/
│ ├── data-table/
│ │ └── data-table.component.ts
│ ├── dialog/
│ │ └── dialog.component.ts
│ ├── empty-state/
│ │ └── empty-state.component.ts
│ ├── error-state/
│ │ └── error-state.component.ts
│ └── form-field/
│ └── form-field.component.ts
└── features/
├── authentication/
│ ├── domain/
│ │ ├── entities/
│ │ │ ├── session.entity.ts
│ │ │ └── user.entity.ts
│ │ └── repository/
│ │ └── iauth.repository.ts
│ ├── application/
│ │ ├── dto/
│ │ │ ├── login-request.dto.ts
│ │ │ ├── login-response.dto.ts
│ │ │ └── register-request.dto.ts
│ │ ├── mappers/
│ │ │ └── auth.mapper.ts
│ │ └── facade/
│ │ └── auth.store.ts
│ ├── infrastructure/
│ │ └── http/
│ │ └── auth-api.service.ts
│ └── presentation/
│ └── pages/
│ ├── login/
│ │ └── login.page.ts
│ └── register/
│ └── register.page.ts
├── dashboard/
│ ├── domain/
│ │ ├── entities/
│ │ │ └── dashboard-stats.entity.ts
│ │ └── repository/
│ │ └── idashboard.repository.ts
│ ├── application/
│ │ └── facade/
│ │ └── dashboard.store.ts
│ ├── infrastructure/
│ │ └── http/
│ │ └── dashboard-api.service.ts
│ └── presentation/
│ └── pages/
│ └── dashboard/
│ └── dashboard.page.ts
├── users/
│ ├── domain/
│ │ ├── entities/
│ │ │ └── user.entity.ts
│ │ └── repository/
│ │ └── iuser.repository.ts
│ ├── application/
│ │ ├── dto/
│ │ │ └── user.dto.ts
│ │ ├── mappers/
│ │ │ └── user.mapper.ts
│ │ └── facade/
│ │ └── users.store.ts
│ ├── infrastructure/
│ │ └── http/
│ │ └── users-api.service.ts
│ └── presentation/
│ └── pages/
│ ├── user-form/
│ │ └── user-form.page.ts
│ └── user-list/
│ └── user-list.page.ts
├── products/
│ ├── domain/
│ │ ├── entities/
│ │ │ └── product.entity.ts
│ │ └── repository/
│ │ └── iproduct.repository.ts
│ ├── application/
│ │ └── facade/
│ │ └── products.store.ts
│ ├── infrastructure/
│ │ └── http/
│ │ └── products-api.service.ts
│ └── presentation/
│ └── pages/
│ └── product-list/
│ └── product-list.page.ts
├── orders/
│ ├── domain/
│ │ ├── entities/
│ │ │ └── order.entity.ts
│ │ └── repository/
│ │ └── iorder.repository.ts
│ ├── application/
│ │ └── facade/
│ │ └── orders.store.ts
│ ├── infrastructure/
│ │ └── http/
│ │ └── orders-api.service.ts
│ └── presentation/
│ └── pages/
│ └── order-list/
│ └── order-list.page.ts
├── notifications/
│ ├── domain/
│ │ ├── entities/
│ │ │ └── notification.entity.ts
│ │ └── repository/
│ │ └── inotification.repository.ts
│ ├── application/
│ │ └── facade/
│ │ └── notifications.store.ts
│ ├── infrastructure/
│ │ └── http/
│ │ └── notifications-api.service.ts
│ └── presentation/
│ └── pages/
│ └── notifications/
│ └── notifications.page.ts
└── profile/
├── domain/
│ ├── entities/
│ │ └── profile.entity.ts
│ └── repository/
│ └── iprofile.repository.ts
├── application/
│ └── facade/
│ └── profile.store.ts
├── infrastructure/
│ └── http/
│ └── profile-api.service.ts
└── presentation/
└── pages/
└── profile/
└── profile.page.ts