This project demonstrates a potential Angular project structure showcasing best (of my knowledge) practices for scalable applications. It includes a feature-based architecture (instead of component-based), state management, and maintainability strategies.
- Project Overview
- Architecture Decisions
- Folder Structure
- State Management
- Design Patterns
- Technologies & Tools
- Getting Started
- Best Practices
- Scalability Notes
This Angular project structure is designed to be:
- Scalable: Easy to add new features without affecting existing code
- Maintainable: Clear separation of concerns and consistent patterns
- Testable: Components and services are easily testable in isolation
- Performant: Lazy loading, OnPush change detection, and optimized bundles
The project follows a feature-based architecture where each feature is self-contained with its own:
- Module (
feature.module.ts) - Routing (
feature-routing.module.ts) - Components
- Services
- Models/Interfaces
- Guards (if feature-specific)
Why?
- Clear boundaries between features
- Easy to locate feature-related code
- Supports lazy loading for better performance
- Enables team members to work on different features independently
Core Module (src/app/core/)
- Contains singleton services, guards, and interceptors
- Imported once in
AppModule - Includes app-wide services like
AuthService,ApiService - Prevents duplicate service instances
Shared Module (src/app/shared/)
- Contains reusable components, directives, and pipes
- Imported by feature modules that need shared functionality
- Examples:
ButtonComponent,LoadingSpinnerComponent - Do NOT import in
CoreModuleorAppModule
Why?
- Prevents circular dependencies
- Clear distinction between app-wide and reusable code
- Better tree-shaking and bundle optimization
Layout Module (src/app/layout/)
- Contains layout components:
HeaderComponent,SidebarComponent,FooterComponent - Imported in
AppModule - Provides consistent application shell
src/
├── app/
│ ├── core/ # Singleton services, guards, interceptors
│ │ ├── guards/
│ │ │ └── auth.guard.ts
│ │ ├── interceptors/
│ │ │ ├── auth.interceptor.ts
│ │ │ └── error.interceptor.ts
│ │ ├── services/
│ │ │ ├── api.service.ts
│ │ │ └── auth.service.ts
│ │ ├── models/
│ │ ├── core.module.ts
│ │ └── index.ts # Barrel export
│ │
│ ├── shared/ # Shared components, directives, pipes
│ │ ├── components/
│ │ │ ├── button/
│ │ │ └── loading-spinner/
│ │ ├── directives/ # Placeholder for custom directives
│ │ ├── pipes/ # Placeholder for custom pipes
│ │ ├── utils/
│ │ ├── shared.module.ts
│ │ └── index.ts # Barrel export
│ │
│ ├── features/ # Feature modules (lazy-loaded)
│ │ └── dashboard/
│ │ ├── components/
│ │ ├── services/
│ │ ├── models/
│ │ ├── dashboard.module.ts
│ │ └── dashboard-routing.module.ts
│ │
│ ├── layout/ # Layout components
│ │ ├── header/
│ │ ├── sidebar/
│ │ ├── footer/
│ │ └── layout.module.ts
│ │
│ ├── store/ # NgRx store
│ │ ├── state/
│ │ │ ├── app.state.ts
│ │ │ └── dashboard.state.ts
│ │ ├── actions/
│ │ │ └── dashboard.actions.ts
│ │ ├── reducers/
│ │ │ ├── dashboard.reducer.ts
│ │ │ └── index.ts
│ │ ├── effects/
│ │ │ ├── dashboard.effects.ts
│ │ │ └── index.ts
│ │ └── index.ts
│ │
│ ├── app.module.ts
│ ├── app-routing.module.ts
│ └── app.component.*
│
├── assets/ # Static assets
├── environments/ # Environment configurations
│ ├── environment.ts
│ └── environment.prod.ts
├── styles/ # Global styles
│ └── styles.scss
└── main.ts # Application entry point
Barrel exports provide clean imports:
// Instead of:
import { AuthGuard } from '@core/guards/auth.guard';
import { ApiService } from '@core/services/api.service';
// Use:
import { AuthGuard, ApiService } from '@core';Configured path aliases for cleaner imports:
@core/*→src/app/core/*@shared/*→src/app/shared/*@features/*→src/app/features/*@environments/*→src/environments/*
For complex applications with shared state, NgRx is recommended:
Structure:
store/
├── state/ # State interfaces
├── actions/ # Action creators
├── reducers/ # State reducers
└── effects/ # Side effects
Benefits:
- Predictable state management
- Time-travel debugging with Redux DevTools
- Centralized state
- Easy to test
- Great for complex state interactions
Example Usage:
// Component
this.store.dispatch(loadDashboardStats());
// Selector
this.stats$ = this.store.select(selectDashboardStats);For simpler state or feature-specific state, use Services with BehaviorSubjects:
Example:
@Injectable()
export class FeatureService {
private stateSubject = new BehaviorSubject<FeatureState>(initialState);
public state$ = this.stateSubject.asObservable();
}When to Use:
- Feature-specific state that doesn't need to be shared
- Simple state management needs
- Smaller applications
Smart Components (Container Components)
- Located in feature modules
- Handle data fetching and business logic
- Connect to services or NgRx store
- Example:
DashboardComponent
Dumb Components (Presentational Components)
- Located in shared module or feature components
- Receive data via
@Input() - Emit events via
@Output() - No direct service dependencies
- Example:
DashboardStatsComponent,ButtonComponent
Components use ChangeDetectionStrategy.OnPush for better performance:
- Only checks for changes when:
- Input references change
- Events occur
- Observables emit (with
asyncpipe)
All services use Angular's dependency injection:
- Services are provided in modules or use
providedIn: 'root' - Easy to mock for testing
- Loose coupling between components and services
- Angular 21: Latest Angular version with modern features
- TypeScript 5.9: Strict mode enabled for type safety
- RxJS: Reactive programming for async operations
- NgRx 20: Store, Effects (for complex state)
- RxJS BehaviorSubjects: For simpler state management
- Angular DevTools: Browser extension for debugging
- TypeScript: Built-in type checking
- Vitest: Fast unit testing with Angular support
- Playwright: E2E testing across multiple browsers
- Node.js (v18 or higher)
- npm or yarn
- Install dependencies:
npm install- Install Playwright browsers (required for E2E tests):
npx playwright install- Downloads browser binaries for Chromium, Firefox, and WebKit
- Required before running E2E tests (
npm run test:e2e) - Only needs to be run once after
npm install
Start Development Server
npm start
# or
npm start -- --port 4201 # Use different port if 4200 is in use- Starts Angular development server on
http://localhost:4200 - Hot module replacement enabled
- Auto-reloads on file changes
Build Application
npm run build- Creates production build in
dist/folder - Optimized and minified for production
Build in Watch Mode
npm run watch- Builds application and watches for changes
- Rebuilds automatically when files change
- Useful for development builds
Run Unit Tests (Once)
npm test
# or
npm run test- Runs all unit tests with Vitest
- Exits after completion
- Shows test results summary
Run Unit Tests in Watch Mode
npm run test:watch- Runs tests in watch mode
- Automatically re-runs tests when files change
- Great for test-driven development
Run Tests with Coverage
npm run test:coverage- Runs all tests and generates coverage report
- Creates coverage report in
coverage/folder - Shows statement, branch, function, and line coverage
Open Test UI (Interactive)
npm run test:ui- Opens Vitest's interactive UI in browser
- Visual test exploration and debugging
- Filter and run specific tests
Run E2E Tests
npm run test:e2e- Runs Playwright end-to-end tests
- Tests across multiple browsers (Chromium, Firefox, WebKit)
- Headless mode by default
- Note: Requires browsers to be installed first with
npx playwright install
Run E2E Tests with UI
npm run test:e2e:ui- Opens Playwright's interactive test UI
- Visual test execution and debugging
- Step through tests visually
Run E2E Tests in Headed Mode
npm run test:e2e:headed- Runs E2E tests with visible browser
- Useful for debugging test failures
- See what the browser is doing during tests
- One component per file
- Component files:
component-name.component.ts|html|scss - Use
OnPushchange detection when possible - Keep components focused and small
- Core services in
core/services/ - Feature services in
features/[feature]/services/ - Use dependency injection
- Make services testable (no side effects in constructors)
- Feature modules are lazy-loaded
- Core module imported once
- Shared module imported by features
- Avoid circular dependencies
- Components: PascalCase (
DashboardComponent) - Services: PascalCase with "Service" suffix (
DashboardService) - Interfaces: PascalCase (
DashboardStats) - Files: kebab-case (
dashboard-stats.component.ts) - Selectors: kebab-case with prefix (
app-dashboard)
- Use TypeScript strict mode
- Prefer interfaces over types for object shapes
- Use async/await for promises
- Use RxJS operators for observables
- Keep functions small and focused
- Create feature folder in
src/app/features/ - Generate feature module:
feature-name/ ├── components/ ├── services/ ├── models/ ├── feature-name.module.ts └── feature-name-routing.module.ts - Add lazy-loaded route in
app-routing.module.ts:{ path: 'feature-name', loadChildren: () => import('./features/feature-name/feature-name.module') .then(m => m.FeatureNameModule) }
- Create state interface in
store/state/ - Create actions in
store/actions/ - Create reducer in
store/reducers/ - Create effects in
store/effects/(if needed) - Register in
app.module.ts
- Create component in
shared/components/ - Declare and export in
shared.module.ts - Import
SharedModulein feature modules that need it
- Lazy Loading: All feature modules are lazy-loaded
- OnPush Strategy: Components use OnPush change detection
- Tree Shaking: Barrel exports help with tree shaking
- Bundle Analysis: Use
ng build --stats-jsonto analyze bundles
- Feature modules are automatically code-split
- Shared module is in a separate chunk
- Consider route-based code splitting for large features
This project uses Vitest for unit testing and Playwright for E2E testing. The test suite demonstrates comprehensive testing practices for Angular applications.
This project migrated from Jest to Vitest for several important reasons:
Performance & Speed:
- Faster execution: Vitest is built on Vite, providing significantly faster test runs, especially in watch mode
- Native ESM support: Better compatibility with modern JavaScript modules
- Optimized for development: Instant feedback during test-driven development
Better Angular Integration:
- Modern tooling: Vitest works seamlessly with Angular 21 and TypeScript 5.9
- Improved compatibility: Better handling of Angular's dependency injection and TestBed
- Future-proof: Active development and better alignment with Angular's direction
Developer Experience:
- Interactive UI: Built-in test UI (
npm run test:ui) for visual test exploration - Better error messages: More informative error reporting
- Hot module replacement: Faster test re-runs during development
Technical Advantages:
- Smaller bundle size: More efficient than Jest for modern projects
- Better TypeScript support: Native TypeScript support without additional configuration
- Consistent tooling: Uses the same build tool (Vite) as modern Angular tooling
Migration Benefits:
- All 31 tests migrated successfully with improved async handling
- Better integration with Angular's testing utilities
- More maintainable test setup with cleaner configuration
The project includes 31 unit tests across 3 test suites, all passing:
Tests the core HTTP communication service that provides a centralized API layer:
- ✅ Service instantiation and dependency injection
- ✅ GET requests with URL construction and query parameters
- ✅ POST requests with request body handling
- ✅ PUT requests for full resource updates
- ✅ DELETE requests for resource removal
- ✅ PATCH requests for partial updates
Key Features Demonstrated:
- HTTP request mocking with
HttpClientTestingModule - Async operation testing with proper promise handling
- Request verification (method, URL, body, query params)
- Environment-based URL configuration
Tests the authentication service managing user authentication state:
- ✅ Service instantiation
- ✅ Initial state detection (authenticated/unauthenticated)
- ✅ Login functionality with token storage
- ✅ Reactive state management with RxJS Observables
- ✅ Logout functionality with token removal and navigation
- ✅ Token retrieval from localStorage
Key Features Demonstrated:
- localStorage integration testing
- RxJS Observable state management
- Router navigation mocking
- Reactive state emission verification
Tests a reusable presentational button component:
- ✅ Component instantiation
- ✅ Input property configuration (type, variant, disabled, loading)
- ✅ Event emission handling
- ✅ Conditional rendering (loading spinner)
- ✅ Template rendering and DOM output
- ✅ Accessibility features (disabled states)
Key Features Demonstrated:
- Presentational component pattern (Smart/Dumb)
- Input/Output property testing
- Template rendering verification
- Change detection with OnPush strategy
- Conditional DOM rendering
- Unit Testing: Each service and component is tested in isolation
- Mocking: HTTP calls, Router, and localStorage are properly mocked
- Async Handling: Uses
async/awaitwith RxJSfirstValueFromfor Observable-based code - Change Detection: Properly handles Angular's change detection in component tests
- Test Isolation: Each test runs in a clean state with proper setup/teardown
# Run all unit tests
npm test
# Run tests in watch mode (auto-reruns on file changes)
npm run test:watch
# Run tests with coverage report
npm run test:coverage
# Open interactive test UI
npm run test:ui
# Run E2E tests
npm run test:e2e
# Run E2E tests with UI
npm run test:e2e:uiRun npm run test:coverage to generate a detailed coverage report showing:
- Statement coverage
- Branch coverage
- Function coverage
- Line coverage
See TESTING.md for detailed testing documentation and best practices.
- Angular Style Guide
- NgRx Documentation
- RxJS Documentation
- Angular Best Practices
- Vitest Documentation
- Angular Testing Guide
- Playwright Documentation
This project is for demonstration purposes.