Angular Architecture — Modules, Components, Templates, and the CLI

Angular is a complete, opinionated platform for building single-page applications — not a library you assemble piecemeal, but a full framework with a prescribed architecture. Understanding how Angular structures an application — the roles of modules, components, templates, services, and the CLI — before writing a single line of code is the difference between building an Angular application and fighting Angular to build an application. This lesson gives you the mental model and practical foundation that every subsequent chapter builds on.

Core Angular Concepts

Concept What It Is Analogy
Component A TypeScript class + HTML template + CSS styles that controls a piece of the UI A reusable UI widget — a task card, a nav bar, a modal
Template HTML with Angular-specific syntax — bindings, directives, pipes A smart HTML file that knows how to display component data
Service A class that provides shared logic — HTTP calls, state, utilities — injected into components A business logic layer decoupled from the UI
Directive A class that modifies the behaviour or appearance of a DOM element *ngIf, *ngFor, or custom attribute directives
Pipe A pure function that transforms display values in templates date | 'mediumDate', currency | 'USD'
Module (NgModule) A container that groups components, directives, and pipes (legacy pre-v14 approach) A feature package — AppModule, TasksModule
Standalone Component A component that declares its own imports — no NgModule needed (Angular 14+) Self-contained feature unit — the modern Angular approach
Router Maps URL paths to components — handles navigation without full page reloads URL-to-component mapping table
Dependency Injection Angular’s mechanism for providing services to components — configured via providers Automatic wiring of dependencies — no manual new Service()

Angular CLI Commands Reference

Command What It Does
ng new app-name --routing --style=scss --standalone Scaffold a new Angular project
ng serve Start the development server on port 4200 with hot reload
ng build --configuration production Build optimised production bundle in dist/
ng generate component feature/name Generate component files (also: ng g c)
ng generate service core/services/name Generate service file (also: ng g s)
ng generate interface shared/models/name Generate TypeScript interface
ng generate guard core/guards/name Generate route guard
ng generate pipe shared/pipes/name Generate pipe
ng test Run unit tests with Karma/Jest
ng lint Run ESLint on the project
ng add @angular/material Add Angular Material component library
ng version Display Angular CLI and framework versions
Note: Angular 14 introduced standalone components — components that declare their own imports without belonging to an NgModule. Angular 17+ made standalone the default for new projects. The standalone: true property in the @Component decorator marks a component as self-contained. New MEAN Stack projects should use standalone components from the start — the code is simpler, lazy loading is easier, and it aligns with Angular’s direction. Legacy NgModule-based code will continue to work but standalone is the modern path.
Tip: Use the Angular CLI for generating every file — never create Angular files manually. The CLI generates the correct file structure, names, and boilerplate, and automatically updates angular.json and routes when appropriate. A single ng generate component features/tasks/task-list creates task-list.component.ts, task-list.component.html, task-list.component.scss, and task-list.component.spec.ts with all the boilerplate correctly wired.
Warning: Never import services directly with new MyService() inside a component. Angular’s dependency injection system manages service instances — singleton services are created once and shared. Manually instantiating a service creates a separate instance that does not share state with the rest of the application, bypasses the injection hierarchy, and makes testing impossible. Always receive services through the constructor or inject() function.

Project Structure

task-manager-frontend/
src/
  app/
    app.config.ts          ← Bootstrap providers: router, http, etc.
    app.routes.ts          ← Top-level route definitions
    app.component.ts       ← Root component — contains <router-outlet>
    app.component.html
    app.component.scss

    core/                  ← Singleton services, interceptors, guards
      services/
        auth.service.ts
        api.service.ts
      interceptors/
        auth.interceptor.ts
        error.interceptor.ts
      guards/
        auth.guard.ts

    features/              ← Feature components — one folder per page/feature
      auth/
        login/
          login.component.ts
          login.component.html
          login.component.scss
        register/
          register.component.ts

      tasks/
        task-list/
          task-list.component.ts
          task-list.component.html
        task-form/
          task-form.component.ts
        task-detail/
          task-detail.component.ts

    shared/                ← Reusable components, pipes, models
      components/
        spinner/
        button/
        modal/
      models/
        task.model.ts      ← TypeScript interfaces
        user.model.ts
      pipes/
        relative-date.pipe.ts

  environments/
    environment.ts         ← Dev: apiUrl = http://localhost:3000
    environment.prod.ts    ← Prod: apiUrl = https://api.yourapp.com
  main.ts                  ← Bootstrap the Angular app
  index.html               ← Single HTML file — Angular injects into <app-root>

Creating and Understanding a Component

// Generate: ng generate component features/tasks/task-list
// Creates 4 files — this is task-list.component.ts:

import { Component, OnInit, signal } from '@angular/core';
import { CommonModule }              from '@angular/common';
import { RouterModule }              from '@angular/router';
import { TaskService }               from '../../../core/services/task.service';
import { Task }                      from '../../../shared/models/task.model';

@Component({
    // ── Selector: how this component is used in other templates ────────────
    // <app-task-list></app-task-list>
    selector:    'app-task-list',

    // ── Standalone: declares its own imports (no NgModule) ─────────────────
    standalone:  true,

    // ── Imports: other components, directives, pipes this template needs ───
    // Only needed for standalone components
    imports:     [CommonModule, RouterModule],

    // ── Template: HTML that this component renders ─────────────────────────
    // Either inline or a separate .html file (recommended for larger templates)
    templateUrl: './task-list.component.html',

    // ── Styles: scoped to this component — do not leak to other components ─
    styleUrl: './task-list.component.scss',
})
export class TaskListComponent implements OnInit {
    // ── Signals for reactive state (Angular 16+) ──────────────────────────
    tasks   = signal<Task[]>([]);
    loading = signal(true);
    error   = signal<string | null>(null);

    // ── Constructor injection: Angular provides the service ────────────────
    constructor(private taskService: TaskService) {}

    // ── Lifecycle hook: runs once after component initialises ──────────────
    ngOnInit(): void {
        this.loadTasks();
    }

    loadTasks(): void {
        this.loading.set(true);
        this.taskService.getAll().subscribe({
            next:  tasks  => { this.tasks.set(tasks); this.loading.set(false); },
            error: err    => { this.error.set(err.message); this.loading.set(false); },
        });
    }

    trackById(_: number, task: Task): string {
        return task._id;
    }
}
<!-- task-list.component.html -->
<div class="task-list">

    <!-- Loading state -->
    <div *ngIf="loading()" class="spinner">Loading tasks...</div>

    <!-- Error state -->
    <div *ngIf="error()" class="error">{{ error() }}</div>

    <!-- Task list -->
    <ul *ngIf="!loading() && !error()">
        <li *ngFor="let task of tasks(); trackBy: trackById">
            <a [routerLink]="['/tasks', task._id]">{{ task.title }}</a>
            <span [class]="'badge badge--' + task.priority">{{ task.priority }}</span>
        </li>
    </ul>

    <!-- Empty state -->
    <p *ngIf="!loading() && tasks().length === 0">No tasks yet.</p>

</div>

Bootstrap: app.config.ts and main.ts

// src/app/app.config.ts — application-wide providers
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter }          from '@angular/router';
import { provideHttpClient,
         withInterceptors }       from '@angular/common/http';
import { routes }                 from './app.routes';
import { authInterceptor }        from './core/interceptors/auth.interceptor';
import { errorInterceptor }       from './core/interceptors/error.interceptor';

export const appConfig: ApplicationConfig = {
    providers: [
        // Performance: use zone.js for change detection (or provideExperimentalZonelessChangeDetection)
        provideZoneChangeDetection({ eventCoalescing: true }),

        // Router with route definitions
        provideRouter(routes),

        // HttpClient with functional interceptors
        provideHttpClient(
            withInterceptors([authInterceptor, errorInterceptor])
        ),
    ],
};

// src/main.ts — bootstrap the application
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent }         from './app/app.component';
import { appConfig }            from './app/app.config';

bootstrapApplication(AppComponent, appConfig)
    .catch(err => console.error(err));

// src/app/app.routes.ts — top-level routes
import { Routes } from '@angular/router';
import { authGuard } from './core/guards/auth.guard';

export const routes: Routes = [
    { path: '', redirectTo: '/tasks', pathMatch: 'full' },

    // Auth routes — eagerly loaded (small, needed immediately)
    {
        path: 'auth',
        children: [
            {
                path:         'login',
                loadComponent: () => import('./features/auth/login/login.component')
                    .then(m => m.LoginComponent),
            },
            {
                path:         'register',
                loadComponent: () => import('./features/auth/register/register.component')
                    .then(m => m.RegisterComponent),
            },
        ],
    },

    // Protected routes — lazy loaded + guarded
    {
        path:    'tasks',
        canActivate: [authGuard],
        children: [
            {
                path:          '',
                loadComponent: () => import('./features/tasks/task-list/task-list.component')
                    .then(m => m.TaskListComponent),
            },
            {
                path:          ':id',
                loadComponent: () => import('./features/tasks/task-detail/task-detail.component')
                    .then(m => m.TaskDetailComponent),
            },
        ],
    },

    // 404 catch-all
    {
        path:          '**',
        loadComponent: () => import('./features/not-found/not-found.component')
            .then(m => m.NotFoundComponent),
    },
];

How It Works

Step 1 — Angular Bootstraps at the Root Component

When the browser loads the Angular application, main.ts calls bootstrapApplication(AppComponent, appConfig). Angular reads appConfig.providers to set up the dependency injection container (router, HTTP client, interceptors). It then renders AppComponent into the <app-root> element in index.html. The root component typically just contains a <router-outlet> — a placeholder where the active route’s component is rendered.

Step 2 — Components Control Pieces of the UI

Each component is responsible for one piece of the user interface. The @Component decorator tells Angular the component’s HTML selector, template, and style files. Angular compiles the template at build time, replacing Angular-specific syntax ({{ }}, *ngIf, [property]) with efficient DOM manipulation code. The component class holds the data the template displays and handles user interactions.

Step 3 — Standalone Components Declare Their Own Dependencies

A standalone component’s imports array lists everything its template needs: CommonModule (for *ngIf, *ngFor), RouterModule (for routerLink), FormsModule (for ngModel), or other standalone components used in the template. This is the component’s explicit dependency declaration — Angular uses it to tree-shake unused code from the bundle at build time.

Step 4 — Services Are Provided and Injected

A service decorated with @Injectable({ providedIn: 'root' }) is a singleton — one instance shared across the entire application. Angular’s DI system creates it the first time it is requested and provides the same instance to every component and service that injects it. Services declared with providedIn: 'root' are automatically available everywhere without any additional configuration.

Step 5 — Lazy Loading Loads Feature Code on Demand

Route definitions with loadComponent: () => import('./features/...') create code-split bundles. The tasks feature’s JavaScript is only downloaded when the user navigates to /tasks — not on initial page load. This dramatically reduces the initial bundle size and time-to-interactive, especially important for large applications with many features.

Real-World Example: app.component.ts (Root)

// src/app/app.component.ts — root component
import { Component }   from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { NavbarComponent } from './shared/components/navbar/navbar.component';

@Component({
    selector:   'app-root',
    standalone: true,
    imports:    [RouterOutlet, NavbarComponent],
    template: `
        <app-navbar></app-navbar>
        <main class="main-content">
            <router-outlet></router-outlet>
        </main>
    `,
    styles: [`
        .main-content {
            max-width: 1200px;
            margin: 0 auto;
            padding: 2rem 1rem;
        }
    `]
})
export class AppComponent {
    title = 'Task Manager';
}

Common Mistakes

Mistake 1 — Manually instantiating services with new

❌ Wrong — creates a separate instance outside Angular’s DI system:

export class TaskListComponent {
    private taskService = new TaskService();  // bypasses DI entirely!
    // No HttpClient is available — TaskService's constructor will fail
}

✅ Correct — inject through the constructor or inject() function:

export class TaskListComponent {
    constructor(private taskService: TaskService) {}
    // OR modern style:
    private taskService = inject(TaskService);
}

Mistake 2 — Forgetting to add components to imports in standalone components

❌ Wrong — child component selector not recognised in template:

@Component({
    standalone: true,
    imports: [CommonModule],  // missing TaskCardComponent!
    template: `<app-task-card [task]="task"></app-task-card>`
})
// Error: 'app-task-card' is not a known element

✅ Correct — include every component, directive, and pipe used in the template:

@Component({
    standalone: true,
    imports: [CommonModule, TaskCardComponent],
    template: `<app-task-card [task]="task"></app-task-card>`
})

Mistake 3 — Putting all routes in one eager bundle

❌ Wrong — all feature code downloaded on initial load:

import { TaskListComponent }   from './features/tasks/task-list.component';
import { AdminDashboard }      from './features/admin/admin.component';
// All imported — all bundled together — slow initial load
const routes = [{ path: 'tasks', component: TaskListComponent }];

✅ Correct — lazy load feature components:

const routes = [{
    path: 'tasks',
    loadComponent: () => import('./features/tasks/task-list.component')
        .then(m => m.TaskListComponent)
}];

Quick Reference

Task CLI Command
New project ng new app --routing --style=scss --standalone
Generate component ng g c features/tasks/task-list
Generate service ng g s core/services/task
Generate interface ng g interface shared/models/task
Generate guard ng g guard core/guards/auth
Generate pipe ng g pipe shared/pipes/relative-date
Serve dev ng serve
Build prod ng build --configuration production
Run tests ng test

🧠 Test Yourself

A standalone Angular component’s template uses *ngFor and a child <app-task-card> component. What must be in the component’s imports array?