Why Angular?
Angular (not to be confused with AngularJS, the older version) is a complete, opinionated framework — unlike React, which is just a library. Angular comes with built-in tools for routing, HTTP client, forms, animations, testing, and more. This makes it particularly popular in enterprise environments where consistency and conventions are valued.
Angular's key advantages:
- Full framework: No need to assemble libraries — everything is included
- TypeScript by default: Catches bugs at compile time, excellent IDE support
- Two-way data binding: Simplified form handling
- Dependency injection: Clean, testable code architecture
- Angular CLI: Scaffolding and build tooling out of the box
- Strong conventions: Large teams stay consistent
Setting Up Angular
Install the Angular CLI globally:
npm install -g @angular/cli@11
ng version # Verify installation
Create a new project:
ng new angular-task-manager --routing --style=scss
cd angular-task-manager
ng serve # Start development server on http://localhost:4200
The "--routing" flag adds an app routing module. "--style=scss" uses SCSS for styles.
Angular Project Structure
src/
├── app/
│ ├── core/ # Singleton services, guards, interceptors
│ │ ├── services/
│ │ ├── guards/
│ │ └── interceptors/
│ ├── shared/ # Shared components, pipes, directives
│ │ ├── components/
│ │ └── pipes/
│ ├── features/ # Feature modules
│ │ ├── tasks/
│ │ │ ├── tasks.module.ts
│ │ │ ├── tasks-list/
│ │ │ ├── task-detail/
│ │ │ └── task-form/
│ │ └── auth/
│ ├── app-routing.module.ts
│ ├── app.module.ts
│ └── app.component.ts
├── assets/
├── environments/
│ ├── environment.ts # Development config
│ └── environment.prod.ts # Production config
└── styles.scss
Understanding Modules
Every Angular app has at least one module — AppModule. Modules organize related components, services, and directives:
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { ReactiveFormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { TasksModule } from './features/tasks/tasks.module';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
HttpClientModule,
ReactiveFormsModule,
AppRoutingModule,
TasksModule,
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
Components: The Building Blocks
Generate a component with the CLI:
ng generate component features/tasks/tasks-list
# Short form: ng g c features/tasks/tasks-list
This creates four files: ".ts", ".html", ".scss", and ".spec.ts" (test).
// tasks-list.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TaskService } from '../../core/services/task.service';
import { Task } from '../../shared/models/task.model';
@Component({
selector: 'app-tasks-list',
templateUrl: './tasks-list.component.html',
styleUrls: ['./tasks-list.component.scss'],
})
export class TasksListComponent implements OnInit, OnDestroy {
tasks: Task[] = [];
loading = false;
private destroy$ = new Subject<void>();
constructor(private taskService: TaskService) {}
ngOnInit(): void {
this.loading = true;
this.taskService.getTasks()
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (tasks) => { this.tasks = tasks; this.loading = false; },
error: (err) => { console.error(err); this.loading = false; },
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
trackByTaskId(index: number, task: Task): number {
return task.id;
}
}
// shared/models/task.model.ts
export interface Task {
id: number;
title: string;
description: string;
status: 'todo' | 'in-progress' | 'done';
priority: 'low' | 'medium' | 'high';
dueDate: string | null;
createdAt: string;
}
Angular Templates
Angular's template syntax extends HTML with data binding and directives:
<!-- tasks-list.component.html -->
<div class="tasks-container">
<h1>Task Manager</h1>
<div *ngIf="loading" class="loading-spinner">
Loading tasks...
</div>
<div *ngIf="!loading && tasks.length === 0" class="empty-state">
<p>No tasks yet. <a routerLink="/tasks/new">Create one!</a></p>
</div>
<ul *ngIf="!loading && tasks.length > 0" class="task-list">
<li *ngFor="let task of tasks; trackBy: trackByTaskId"
[class]="'task-item task-' + task.status"
[class.overdue]="isOverdue(task)">
<div class="task-header">
<h3>{{ task.title }}</h3>
<span class="priority-badge priority-{{ task.priority }}">
{{ task.priority | titlecase }}
</span>
</div>
<p>{{ task.description | slice:0:100 }}{{ task.description.length > 100 ? '...' : '' }}</p>
<div class="task-footer">
<span *ngIf="task.dueDate">Due: {{ task.dueDate | date:'mediumDate' }}</span>
<div class="task-actions">
<button [routerLink]="['/tasks', task.id]">View</button>
<button (click)="deleteTask(task.id)" class="btn-danger">Delete</button>
</div>
</div>
</li>
</ul>
</div>
Key template syntax:
- {{ expression }} — interpolation (one-way binding: component → template)
- [property]="expression" — property binding
- (event)="handler()" — event binding
- [(ngModel)]="property" — two-way binding (requires FormsModule)
- *ngIf="condition" — conditional rendering
- *ngFor="let item of items" — list rendering
- | pipe — data transformation (date, titlecase, currency, etc.)
- routerLink — client-side navigation
Services and Dependency Injection
Services hold business logic and are injected into components:
// core/services/task.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { environment } from '../../../environments/environment';
import { Task } from '../../shared/models/task.model';
@Injectable({
providedIn: 'root', // Singleton across the app
})
export class TaskService {
private apiUrl = `${environment.apiUrl}/tasks`;
constructor(private http: HttpClient) {}
getTasks(status?: string): Observable<Task[]> {
let params = new HttpParams();
if (status) params = params.set('status', status);
return this.http.get<Task[]>(this.apiUrl, { params }).pipe(
catchError(err => throwError(() => new Error(err.error.message)))
);
}
getTask(id: number): Observable<Task> {
return this.http.get<Task>(`${this.apiUrl}/${id}`);
}
createTask(task: Omit<Task, 'id' | 'createdAt'>): Observable<Task> {
return this.http.post<Task>(this.apiUrl, task);
}
updateTask(id: number, changes: Partial<Task>): Observable<Task> {
return this.http.patch<Task>(`${this.apiUrl}/${id}`, changes);
}
deleteTask(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
}
Routing
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from './core/guards/auth.guard';
const routes: Routes = [
{ path: '', redirectTo: '/tasks', pathMatch: 'full' },
{
path: 'tasks',
loadChildren: () => import('./features/tasks/tasks.module')
.then(m => m.TasksModule),
canActivate: [AuthGuard],
},
{
path: 'auth',
loadChildren: () => import('./features/auth/auth.module')
.then(m => m.AuthModule),
},
{ path: '**', redirectTo: '/tasks' }, // Wildcard route
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
Lazy loading (loadChildren) splits the code into separate bundles — the tasks module isn't downloaded until the user navigates to "/tasks".
Reactive Forms
Reactive forms are Angular's powerful approach to form handling, using TypeScript classes to define form structure:
// task-form.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { TaskService } from '../../core/services/task.service';
@Component({
selector: 'app-task-form',
templateUrl: './task-form.component.html',
})
export class TaskFormComponent implements OnInit {
taskForm!: FormGroup;
saving = false;
constructor(
private fb: FormBuilder,
private taskService: TaskService,
private router: Router,
) {}
ngOnInit(): void {
this.taskForm = this.fb.group({
title: ['', [Validators.required, Validators.minLength(3), Validators.maxLength(100)]],
description: ['', Validators.maxLength(500)],
status: ['todo', Validators.required],
priority: ['medium', Validators.required],
dueDate: [null],
});
}
get title() { return this.taskForm.get('title')!; }
onSubmit(): void {
if (this.taskForm.invalid) { this.taskForm.markAllAsTouched(); return; }
this.saving = true;
this.taskService.createTask(this.taskForm.value).subscribe({
next: (task) => this.router.navigate(['/tasks', task.id]),
error: () => { this.saving = false; },
});
}
}
<!-- task-form.component.html -->
<form [formGroup]="taskForm" (ngSubmit)="onSubmit()">
<div class="form-field">
<label for="title">Title *</label>
<input id="title" formControlName="title" type="text" />
<div *ngIf="title.invalid && title.touched" class="errors">
<span *ngIf="title.errors?.['required']">Title is required.</span>
<span *ngIf="title.errors?.['minlength']">At least 3 characters.</span>
</div>
</div>
<div class="form-field">
<label for="priority">Priority</label>
<select id="priority" formControlName="priority">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
<button type="submit" [disabled]="saving">
{{ saving ? 'Saving...' : 'Create Task' }}
</button>
</form>
HTTP Interceptors
Interceptors transform outgoing requests and incoming responses globally:
// core/interceptors/auth.interceptor.ts
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler } from '@angular/common/http';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<unknown>, next: HttpHandler) {
const token = localStorage.getItem('token');
if (token) {
const authReq = req.clone({
headers: req.headers.set('Authorization', `Bearer ${token}`),
});
return next.handle(authReq);
}
return next.handle(req);
}
}
Register in AppModule:
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
]
Building for Production
ng build --configuration production
This creates an optimized /dist folder with:
- Ahead-of-Time (AOT) compilation
- Tree-shaking (removes unused code)
- Minification and compression
- Source maps (optional)
Deploy the dist/ folder to any static hosting (Nginx, Apache, Netlify, Vercel, Firebase Hosting).
For Nginx, add this configuration to handle Angular's client-side routing:
location / {
try_files $uri $uri/ /index.html;
}