Angular 11 Tutorial: Build a Project from Scratch

Angular 11 Tutorial: Build a Project from Scratch

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;
}
Share: