Angular powers some of the world's most demanding enterprise applications — Google's own products, Microsoft Office web, IBM, and thousands of large-scale business platforms rely on it daily. With Angular 18 bringing Signals, Standalone Components stable, and built-in control flow, 2025 is the best time to invest in Angular skills. This roadmap shows exactly what to learn, in what order, with realistic timelines.
At a glance
| Phase |
Topics |
Timeline |
| 1 |
TypeScript & web foundations |
Weeks 1–4 |
| 2 |
Angular core: components & templates |
Weeks 5–8 |
| 3 |
Angular services & dependency injection |
Weeks 9–11 |
| 4 |
Routing & navigation |
Weeks 12–13 |
| 5 |
Forms: Template-driven & Reactive |
Weeks 14–16 |
| 6 |
RxJS & HTTP |
Weeks 17–20 |
| 7 |
State management (Signals + NgRx) |
Weeks 21–23 |
| 8 |
Testing (Jasmine + Karma + Cypress) |
Weeks 24–26 |
| 9 |
Performance & Advanced Angular |
Weeks 27–29 |
| 10 |
DevOps & deployment |
Weeks 30–32 |
Phase 1 — TypeScript & web foundations (Weeks 1–4)
Angular is TypeScript-first. You need TypeScript fluency before Angular makes sense.
TypeScript essentials
// Types, interfaces, classes
interface User {
id: number;
name: string;
email?: string; // optional
}
class UserService {
private users: User[] = [];
getUser(id: number): User | undefined {
return this.users.find(u => u.id === id);
}
}
// Generics
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
// Decorators (Angular uses these everywhere)
function Log(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${key} with`, args);
return original.apply(this, args);
};
return descriptor;
}
| TypeScript concept |
Why it matters in Angular |
| Interfaces & types |
Component inputs/outputs, API models |
| Generics |
Observable<T>, HttpClient.get<T>() |
| Decorators |
@Component, @Injectable, @Input |
| Access modifiers |
Service encapsulation |
| Enums |
Route paths, status codes |
| Type assertions |
Template type narrowing |
When you're ready: You can define interfaces for API responses, write typed class methods, and understand decorator syntax.
HTML & CSS prerequisites
- Semantic HTML5 (forms, tables, accessibility)
- CSS Flexbox and Grid
- CSS custom properties
Phase 2 — Angular core: components & templates (Weeks 5–8)
Setting up Angular 18
npm install -g @angular/cli
ng new my-app --standalone --style=scss
cd my-app
ng serve
Standalone components (Angular 17+)
import { Component, signal, computed } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-counter',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<h2>Count: {{ count() }}</h2>
<p>Double: {{ double() }}</p>
<button (click)="increment()">+</button>
<button (click)="decrement()">-</button>
`
})
export class CounterComponent {
count = signal(0);
double = computed(() => this.count() * 2);
increment() { this.count.update(c => c + 1); }
decrement() { this.count.update(c => c - 1); }
}
Template syntax cheat sheet
| Syntax |
Purpose |
Example |
{{ expr }} |
Interpolation |
{{ user.name }} |
[prop]="expr" |
Property binding |
[disabled]="isLoading" |
(event)="handler()" |
Event binding |
(click)="save()" |
[(ngModel)] |
Two-way binding |
[(ngModel)]="email" |
@if / @else |
Conditional (Angular 17+) |
@if (isLoggedIn) { } |
@for |
Iteration (Angular 17+) |
@for (item of items; track item.id) { } |
@switch |
Switch (Angular 17+) |
@switch (status) { } |
*ngIf (legacy) |
Conditional |
*ngIf="condition" |
*ngFor (legacy) |
Iteration |
*ngFor="let item of items" |
[class.active] |
Conditional class |
[class.active]="isActive" |
[style.color] |
Style binding |
[style.color]="textColor" |
#ref |
Template reference |
#emailInput |
New control flow syntax (Angular 17+)
<!-- @if / @else if / @else -->
@if (user.role === 'admin') {
<app-admin-panel />
} @else if (user.role === 'editor') {
<app-editor-panel />
} @else {
<app-viewer-panel />
}
<!-- @for with track -->
@for (product of products; track product.id) {
<app-product-card [product]="product" />
} @empty {
<p>No products found.</p>
}
<!-- @switch -->
@switch (status) {
@case ('loading') { <app-spinner /> }
@case ('error') { <app-error [message]="errorMsg" /> }
@default { <app-content /> }
}
Phase 3 — Services & dependency injection (Weeks 9–11)
Creating and injecting services
// user.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface User {
id: number;
name: string;
email: string;
}
@Injectable({
providedIn: 'root' // singleton, available app-wide
})
export class UserService {
private http = inject(HttpClient);
private apiUrl = 'https://api.example.com/users';
getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl);
}
getUserById(id: number): Observable<User> {
return this.http.get<User>(`${this.apiUrl}/${id}`);
}
createUser(user: Omit<User, 'id'>): Observable<User> {
return this.http.post<User>(this.apiUrl, user);
}
}
// Using inject() in component (Angular 14+)
@Component({ /* ... */ })
export class UserListComponent {
private userService = inject(UserService);
users = signal<User[]>([]);
ngOnInit() {
this.userService.getUsers().subscribe(users => {
this.users.set(users);
});
}
}
Dependency injection providers
| Provider scope |
Decorator / config |
Use case |
| Root (singleton) |
providedIn: 'root' |
Shared services (auth, API) |
| Component |
providers: [MyService] in @Component |
Per-component instance |
| Module |
providers: [MyService] in @NgModule |
Module-scoped (legacy) |
| Environment |
providers in app.config.ts |
Standalone app config |
inject() vs constructor injection
// Modern: inject() function (preferred in Angular 14+)
export class ProductComponent {
private service = inject(ProductService);
private router = inject(Router);
}
// Classic: constructor injection (still valid)
export class ProductComponent {
constructor(
private service: ProductService,
private router: Router
) {}
}
Phase 4 — Routing & navigation (Weeks 12–13)
Setting up routes in standalone Angular
// app.routes.ts
import { Routes } from '@angular/router';
import { authGuard } from './guards/auth.guard';
export const routes: Routes = [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: 'home', loadComponent: () =>
import('./pages/home/home.component').then(m => m.HomeComponent) },
{
path: 'products',
loadComponent: () =>
import('./pages/products/products.component').then(m => m.ProductsComponent),
children: [
{ path: ':id', loadComponent: () =>
import('./pages/product-detail/product-detail.component').then(m => m.ProductDetailComponent) }
]
},
{
path: 'dashboard',
loadComponent: () =>
import('./pages/dashboard/dashboard.component').then(m => m.DashboardComponent),
canActivate: [authGuard]
},
{ path: '**', loadComponent: () =>
import('./pages/not-found/not-found.component').then(m => m.NotFoundComponent) }
];
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withComponentInputBinding()),
provideHttpClient()
]
};
Router navigation
// In template
<a routerLink="/products">Products</a>
<a [routerLink]="['/products', product.id]">View</a>
<a routerLink="/products" [queryParams]="{ sort: 'price' }">Sorted</a>
// In component
export class NavComponent {
private router = inject(Router);
private route = inject(ActivatedRoute);
goToProduct(id: number) {
this.router.navigate(['/products', id]);
}
// Read route params (Angular 16+ with input binding)
// Just declare @Input() id!: string; when using withComponentInputBinding()
}
Route guards
// auth.guard.ts (functional guard — Angular 15+)
import { inject } from '@angular/core';
import { Router, CanActivateFn } from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isLoggedIn()) return true;
return router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } });
};
Phase 5 — Forms: Template-driven & Reactive (Weeks 14–16)
Angular has two form systems. Reactive forms are preferred for complex use cases.
Template-driven forms (simple cases)
<form #loginForm="ngForm" (ngSubmit)="onSubmit(loginForm)">
<input name="email" [(ngModel)]="email" required email #emailCtrl="ngModel" />
<p *ngIf="emailCtrl.invalid && emailCtrl.touched">Invalid email</p>
<input name="password" [(ngModel)]="password" type="password" required minlength="8" />
<button type="submit" [disabled]="loginForm.invalid">Login</button>
</form>
Reactive forms (recommended for production)
import { Component, inject } from '@angular/core';
import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
@Component({
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="submit()">
<input formControlName="email" placeholder="Email" />
@if (form.get('email')?.hasError('required') && form.get('email')?.touched) {
<span>Email is required</span>
}
@if (form.get('email')?.hasError('email')) {
<span>Invalid email format</span>
}
<input formControlName="password" type="password" placeholder="Password" />
<div formGroupName="address">
<input formControlName="street" />
<input formControlName="city" />
</div>
<button [disabled]="form.invalid">Submit</button>
</form>
`
})
export class LoginFormComponent {
private fb = inject(FormBuilder);
form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
address: this.fb.group({
street: [''],
city: ['', Validators.required]
})
});
submit() {
if (this.form.valid) {
console.log(this.form.value);
}
}
}
| Feature |
Template-driven |
Reactive |
| Setup |
FormsModule |
ReactiveFormsModule |
| Form definition |
HTML template |
Component class |
| Testing |
Hard (DOM-dependent) |
Easy (pure TypeScript) |
| Dynamic fields |
Complex |
Simple with FormArray |
| Validation |
HTML attributes |
Validator functions |
| Best for |
Simple login forms |
Complex, dynamic forms |
Phase 6 — RxJS & HTTP (Weeks 17–20)
RxJS is Angular's reactive primitive. You must understand it to work effectively with Angular.
Essential RxJS operators
import {
Observable, of, from, interval, Subject, BehaviorSubject
} from 'rxjs';
import {
map, filter, tap, switchMap, mergeMap, concatMap,
catchError, debounceTime, distinctUntilChanged,
takeUntil, take, shareReplay, combineLatest, forkJoin
} from 'rxjs/operators';
// Transform
const doubled$ = of(1, 2, 3).pipe(map(x => x * 2));
// Filter
const evens$ = of(1, 2, 3, 4).pipe(filter(x => x % 2 === 0));
// Switch inner observable (cancels previous — use for search)
searchTerm$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.apiService.search(term))
).subscribe(results => this.results.set(results));
// Merge inner observables (doesn't cancel — use for parallel requests)
ids$.pipe(
mergeMap(id => this.apiService.getItem(id))
)
// Wait for all (like Promise.all)
forkJoin({
users: this.userService.getUsers(),
products: this.productService.getProducts()
}).subscribe(({ users, products }) => { /* ... */ });
// Combine latest values from multiple streams
combineLatest([filter$, sort$, page$]).pipe(
switchMap(([filter, sort, page]) => this.apiService.getProducts({ filter, sort, page }))
)
// Error handling
this.http.get('/api/data').pipe(
catchError(err => {
console.error(err);
return of([]); // fallback value
})
)
// Cache and share
const users$ = this.http.get<User[]>('/api/users').pipe(
shareReplay(1) // cache last emission, share among subscribers
);
// Unsubscribe on destroy
private destroy$ = new Subject<void>();
ngOnInit() {
this.dataStream$.pipe(
takeUntil(this.destroy$)
).subscribe(data => this.data.set(data));
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
switchMap vs mergeMap vs concatMap vs exhaustMap
| Operator |
Cancels previous? |
Use case |
switchMap |
Yes |
Search/autocomplete, latest request wins |
mergeMap |
No |
Parallel independent requests |
concatMap |
No (queues) |
Sequential order matters |
exhaustMap |
Ignores new while active |
Login button (prevent double submit) |
Angular Signals (Angular 16+)
import { signal, computed, effect, toSignal, toObservable } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({ /* ... */ })
export class ProductsComponent {
private productService = inject(ProductService);
// Convert Observable to Signal (auto-unsubscribes)
products = toSignal(this.productService.getProducts(), { initialValue: [] });
// Derived signal
productCount = computed(() => this.products().length);
// Effect (runs when dependencies change)
constructor() {
effect(() => {
console.log(`Product count changed: ${this.productCount()}`);
});
}
}
Phase 7 — State management: Signals + NgRx (Weeks 21–23)
Signals for local state (preferred for most cases)
// Simple component state with Signals
@Component({ /* ... */ })
export class CartComponent {
private cartService = inject(CartService);
items = this.cartService.items; // signal from service
total = computed(() =>
this.items().reduce((sum, item) => sum + item.price * item.quantity, 0)
);
addItem(product: Product) {
this.cartService.addItem(product);
}
removeItem(id: number) {
this.cartService.removeItem(id);
}
}
// Service with signal-based state
@Injectable({ providedIn: 'root' })
export class CartService {
private _items = signal<CartItem[]>([]);
items = this._items.asReadonly();
addItem(product: Product) {
this._items.update(items => {
const existing = items.find(i => i.id === product.id);
if (existing) {
return items.map(i => i.id === product.id
? { ...i, quantity: i.quantity + 1 }
: i
);
}
return [...items, { ...product, quantity: 1 }];
});
}
removeItem(id: number) {
this._items.update(items => items.filter(i => i.id !== id));
}
}
NgRx (for complex enterprise state)
// 1. Define state & actions
export interface ProductState {
products: Product[];
loading: boolean;
error: string | null;
}
export const loadProducts = createAction('[Products] Load');
export const loadProductsSuccess = createAction(
'[Products] Load Success',
props<{ products: Product[] }>()
);
export const loadProductsFailure = createAction(
'[Products] Load Failure',
props<{ error: string }>()
);
// 2. Reducer
export const productReducer = createReducer(
{ products: [], loading: false, error: null } as ProductState,
on(loadProducts, state => ({ ...state, loading: true, error: null })),
on(loadProductsSuccess, (state, { products }) => ({ ...state, products, loading: false })),
on(loadProductsFailure, (state, { error }) => ({ ...state, error, loading: false }))
);
// 3. Selectors
export const selectProducts = (state: AppState) => state.products.products;
export const selectLoading = (state: AppState) => state.products.loading;
// 4. Effects
@Injectable()
export class ProductEffects {
private actions$ = inject(Actions);
private productService = inject(ProductService);
loadProducts$ = createEffect(() =>
this.actions$.pipe(
ofType(loadProducts),
switchMap(() => this.productService.getProducts().pipe(
map(products => loadProductsSuccess({ products })),
catchError(err => of(loadProductsFailure({ error: err.message })))
))
)
);
}
// 5. Using in component
@Component({ /* ... */ })
export class ProductListComponent {
private store = inject(Store);
products$ = this.store.select(selectProducts);
loading$ = this.store.select(selectLoading);
ngOnInit() {
this.store.dispatch(loadProducts());
}
}
| State solution |
Best for |
Complexity |
| Component signals |
Local UI state |
Low |
| Service signals |
Shared simple state |
Low |
| NgRx Signals Store |
Medium app state |
Medium |
| NgRx (full) |
Complex enterprise state |
High |
| Akita / Elf |
Alternative to NgRx |
Medium |
Phase 8 — Testing (Weeks 24–26)
Unit testing with Jasmine + TestBed
// counter.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CounterComponent } from './counter.component';
describe('CounterComponent', () => {
let component: CounterComponent;
let fixture: ComponentFixture<CounterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CounterComponent] // standalone component
}).compileComponents();
fixture = TestBed.createComponent(CounterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should start at 0', () => {
expect(component.count()).toBe(0);
});
it('should increment', () => {
component.increment();
expect(component.count()).toBe(1);
});
it('should render count in template', () => {
component.count.set(5);
fixture.detectChanges();
const h2 = fixture.nativeElement.querySelector('h2');
expect(h2.textContent).toContain('5');
});
});
Testing services with mocks
// user.service.spec.ts
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
UserService,
provideHttpClientTesting()
]
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => httpMock.verify());
it('should fetch users', () => {
const mockUsers: User[] = [{ id: 1, name: 'Alice', email: 'alice@example.com' }];
service.getUsers().subscribe(users => {
expect(users).toEqual(mockUsers);
});
const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});
});
E2E with Cypress
// cypress/e2e/auth.cy.ts
describe('Authentication', () => {
it('logs in successfully', () => {
cy.visit('/login');
cy.get('[data-testid="email"]').type('user@example.com');
cy.get('[data-testid="password"]').type('password123');
cy.get('[data-testid="submit"]').click();
cy.url().should('include', '/dashboard');
cy.get('h1').should('contain', 'Welcome');
});
it('shows error for invalid credentials', () => {
cy.visit('/login');
cy.get('[data-testid="email"]').type('wrong@example.com');
cy.get('[data-testid="password"]').type('wrongpassword');
cy.get('[data-testid="submit"]').click();
cy.get('[data-testid="error"]').should('be.visible');
});
});
| Testing level |
Tool |
What to test |
| Unit |
Jasmine + TestBed |
Services, pipes, component logic |
| Component |
Jasmine + ComponentFixture |
Template rendering, user interactions |
| Integration |
Jasmine + HttpTestingController |
HTTP interactions |
| E2E |
Cypress or Playwright |
Critical user flows |
Phase 9 — Performance & advanced Angular (Weeks 27–29)
Change detection strategies
// OnPush: re-renders only when inputs change or signals change
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<h1>{{ title }}</h1><p>{{ count() }}</p>`
})
export class OptimizedComponent {
@Input() title = '';
count = signal(0);
// Only re-renders when:
// 1. @Input() reference changes
// 2. Signal value changes
// 3. Async pipe emits
// 4. markForCheck() is called
}
Performance tips
| Technique |
Impact |
How |
OnPush change detection |
High |
changeDetection: ChangeDetectionStrategy.OnPush |
| Signals (Angular 16+) |
High |
Replace BehaviorSubject with signal() |
| Lazy loading routes |
High |
loadComponent: () => import(...) |
trackBy / track in lists |
High |
@for (item of items; track item.id) |
shareReplay(1) |
Medium |
Cache HTTP responses shared by multiple components |
| Virtual scrolling |
High |
@angular/cdk/scrolling <cdk-virtual-scroll-viewport> |
| Pure pipes |
Medium |
Use instead of method calls in templates |
defer blocks (Angular 17+) |
High |
Lazy load non-critical UI |
Deferred loading (Angular 17+)
<!-- Load heavy component only when in viewport -->
@defer (on viewport) {
<app-heavy-chart [data]="chartData" />
} @placeholder {
<div class="chart-placeholder">Loading chart...</div>
} @loading (minimum 200ms) {
<app-spinner />
} @error {
<p>Failed to load chart.</p>
}
Angular Universal (SSR)
ng add @angular/ssr
ng build && node dist/server/server.mjs
Phase 10 — DevOps & deployment (Weeks 30–32)
Production build & Docker
# Build for production
ng build --configuration production
# Output: dist/my-app/browser/
# Analyze bundle size
ng build --stats-json
npx webpack-bundle-analyzer dist/my-app/browser/stats.json
# Dockerfile (multi-stage)
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build -- --configuration production
FROM nginx:alpine
COPY nginx.conf /etc/nginx/nginx.conf
COPY --from=build /app/dist/my-app/browser /usr/share/nginx/html
EXPOSE 80
GitHub Actions CI/CD
# .github/workflows/ci.yml
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm test -- --watch=false --browsers=ChromeHeadless
- run: npm run build -- --configuration production
deploy:
needs: build-and-test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci && npm run build -- --configuration production
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: ${{ secrets.GITHUB_TOKEN }}
firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
channelId: live
Deployment options
| Platform |
Best for |
Angular support |
| Firebase Hosting |
SPA, SSR via Cloud Functions |
Excellent (Google product) |
| Vercel |
SPA, SSR |
Good |
| Netlify |
SPA |
Good |
| AWS Amplify |
Enterprise |
Good |
| Azure Static Web Apps |
Microsoft stack |
Excellent |
| Self-hosted (Nginx) |
Full control |
Excellent |
Full technology map
Angular 18+ Technology Map
├── Language
│ ├── TypeScript (required)
│ └── HTML / CSS / SCSS
├── Core Angular
│ ├── Standalone Components
│ ├── Signals (reactivity)
│ ├── Dependency Injection
│ ├── Angular Router
│ └── Angular HTTP Client
├── Forms
│ ├── Reactive Forms
│ └── Template-driven Forms
├── Reactive Programming
│ ├── RxJS (Observables, operators)
│ └── Signals + toSignal/toObservable
├── State Management
│ ├── Service + Signals (local/shared)
│ └── NgRx (enterprise)
├── UI Libraries
│ ├── Angular Material
│ ├── PrimeNG
│ └── Tailwind CSS
├── Testing
│ ├── Jasmine + Karma (unit)
│ ├── Angular Testing Library
│ └── Cypress / Playwright (E2E)
├── Build Tools
│ ├── Angular CLI
│ ├── esbuild (Angular 17+ default)
│ └── Nx (monorepo)
├── SSR / SSG
│ └── Angular Universal / @angular/ssr
└── DevOps
├── Docker + Nginx
├── GitHub Actions / GitLab CI
└── Firebase / Vercel / Azure
Realistic 32-week timeline
| Weeks |
Milestone |
What you can build |
| 1–4 |
TypeScript confident |
Typed utilities, classes |
| 5–8 |
Angular basics |
Simple CRUD app with components |
| 9–11 |
Services + DI |
App with shared data layer |
| 12–13 |
Router |
Multi-page SPA |
| 14–16 |
Forms |
Login, registration, complex forms |
| 17–20 |
RxJS + HTTP |
Real API integration with error handling |
| 21–23 |
State management |
Production-grade state with Signals |
| 24–26 |
Testing |
80%+ coverage, E2E flows |
| 27–29 |
Performance |
OnPush, lazy loading, SSR |
| 30–32 |
DevOps |
Dockerized CI/CD pipeline |
Portfolio project ideas
| Project |
Angular concepts practiced |
Complexity |
| Task manager |
Components, forms, local storage |
Beginner |
| Blog with Markdown |
Router, HTTP, pipes |
Beginner |
| E-commerce shop |
NgRx/Signals, cart state, lazy routes |
Intermediate |
| Real-time chat |
WebSockets, RxJS, Signals |
Intermediate |
| Admin dashboard |
Complex forms, tables, charts, permissions |
Advanced |
| SaaS platform |
Auth, roles, SSR, multi-tenant |
Advanced |
Angular developer roles & salaries
| Role |
Experience |
US Salary |
EU Salary |
| Junior Angular Developer |
0–2 yr |
$60–85k |
€35–55k |
| Mid Angular Developer |
2–4 yr |
$85–120k |
€55–80k |
| Senior Angular Developer |
4–7 yr |
$120–160k |
€80–110k |
| Angular Architect |
7+ yr |
$150–200k |
€100–140k |
| Full-Stack (Angular + Node/Java) |
3–6 yr |
$100–150k |
€70–105k |
| Tech Lead / Principal |
8+ yr |
$170–220k |
€120–160k |
Enterprise premium: Angular developers often earn 10–15% more than equivalent React developers in enterprise/banking/healthcare sectors where Angular dominates.
Common mistakes
| Mistake |
Problem |
Fix |
| Skipping TypeScript |
Angular makes no sense without it |
Learn TypeScript first (2–4 weeks) |
Using any everywhere |
Defeats Angular's type safety |
Use proper interfaces |
| Subscribing without unsubscribing |
Memory leaks |
Use takeUntil, async pipe, or toSignal() |
Not using OnPush |
Slow apps with large lists |
Apply OnPush by default |
*ngFor without trackBy/track |
Re-renders entire list on any change |
Always use track item.id |
| Calling methods in templates |
Runs on every change detection |
Use computed signals or pure pipes |
| One giant module/component |
Hard to maintain and test |
One responsibility per component |
| Ignoring the Angular CLI |
Manual setup errors |
Use ng generate for everything |
Angular vs React vs Vue
| Dimension |
Angular |
React |
Vue |
| Type |
Full framework |
UI library |
Progressive framework |
| Language |
TypeScript (required) |
JS/TS |
JS/TS |
| Learning curve |
Steep |
Moderate |
Gentle |
| Opinions |
Highly opinionated |
Minimal |
Moderate |
| State management |
Signals + NgRx |
Context, Zustand, Redux |
Pinia |
| Forms |
Built-in (Reactive + Template) |
React Hook Form, Formik |
Built-in |
| SSR |
Angular Universal / @angular/ssr |
Next.js |
Nuxt.js |
| Best for |
Enterprise, large teams |
Large ecosystem, flexibility |
Smaller teams, Asia market |
| Company backing |
Google |
Meta |
Community |
| Job market (2025) |
~25k US jobs |
~80k US jobs |
~15k US jobs |
Frequently asked questions
Do I need to know Angular-specific JavaScript, or is TypeScript enough?
TypeScript is the primary language. You also need solid understanding of the JavaScript event loop, Promises, and async/await since RxJS and Angular HTTP are async-heavy.
Is Angular still relevant in 2025 with all the React hype?
Yes — Angular is the dominant choice in enterprise, banking, healthcare, government, and large European companies. Google, Microsoft, IBM, Cisco, and Deutsche Bank use Angular extensively. Job demand is stable and salaries are high.
Should I learn NgRx or is Signals enough?
For most applications, Angular Signals (with a signal-based service) cover 80% of state management needs. Learn NgRx when your team needs strict action history, time-travel debugging, or already has an NgRx codebase.
What's the difference between Angular 2+ and AngularJS?
AngularJS (Angular 1.x, 2010) was a completely different framework built on MV* patterns. Angular 2+ (2016–today) is a complete rewrite in TypeScript with a component-based architecture. They share only the name — if you see "Angular" today, it always means Angular 2+.
How long does it realistically take to get an Angular job?
With consistent study (2–3 hours/day), most people are job-ready in 9–12 months. A strong TypeScript background and a portfolio of 2–3 real projects are the biggest accelerators.
Should I learn Angular or React first?
If you want maximum job options, learn React first (3× more jobs). If you want enterprise/government jobs or you are in Europe, Angular is an excellent first choice. Both share similar concepts (components, services, TypeScript) so learning one helps with the other.