Angular is not a library you sprinkle onto a page — it is a complete, opinionated application platform. Where React and Vue leave routing, state management, and dependency injection as ecosystem choices, Angular ships all three as first-class, integrated primitives. That opinionation is the entire value proposition for enterprise teams: a new hire on any Angular codebase already knows where the HTTP layer lives, how forms are validated, and how a service gets injected — because the framework, not the team's internal conventions, made those decisions.


Deconstructing the Angular Architecture

Every Angular file is a plain TypeScript ES module — it has its own import/export statements and is compiled independently by the TypeScript compiler. That is the file-level unit. Angular then layers a second, logical grouping boundary on top of the file system: a compilation context that determines which components, directives, and pipes are visible to a given template. Historically that boundary was the NgModule (@NgModule); as of Angular 14+ (and the default since Angular 17+), it is the Standalone Component itself.

Two Independent Module Systems, Working Together

  • TypeScript/JavaScript ES modules control what code is importable and how the bundler performs tree-shaking — this exists regardless of which Angular architecture you use
  • Angular's own compilation boundary (NgModule or a standalone component's imports array) controls what a template can reference — a component can be exported from its .ts file yet still be invisible to a template that hasn't imported it into its Angular-level scope
  • A Standalone Component collapses the two boundaries into one: the component's imports: [...] array does double duty as both the ES module dependency list and the Angular template compilation scope
  • An NgModule, by contrast, keeps the boundaries separate — a component's TypeScript file exports the class, but a different file (the module) declares it into the Angular compiler's view

Project Initialization & Workspace Orchestration

The Angular CLI (@angular/cli) is not optional tooling — it is the canonical way enterprise Angular projects are scaffolded, built, tested, and upgraded. Running ng update against a CLI-scaffolded workspace is a supported, automated migration path; hand-rolled Webpack configs lose that guarantee entirely.

terminalbash
# Install the CLI globally (or use npx without a global install)
npm install -g @angular/cli@latest

# Scaffold a new enterprise workspace
# --routing:      generates app.routes.ts up front
# --style=css:    plain CSS, no framework-specific preprocessor tax
# --strict:       enables strict TypeScript + stricter Angular compiler checks
# --ssr=false:    disable server-side rendering for this walkthrough (enable per-project as needed)
ng new enterprise-ops-console --routing --style=css --strict --ssr=false

cd enterprise-ops-console

# Verify the workspace builds and serves
ng serve --open

angular.json — The Workspace Build Graph

angular.json is the single source of truth for how every project in the workspace is built, served, tested, and linted. In a multi-app enterprise workspace (e.g. a customer-facing app and an internal admin console sharing a library), this file defines a project entry per application and per library.

angular.json (relevant excerpt)json
{
  "projects": {
    "enterprise-ops-console": {
      "projectType": "application",
      "root": "",
      "sourceRoot": "src",
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:application",
          "options": {
            "outputPath": "dist/enterprise-ops-console",
            "index": "src/index.html",
            "browser": "src/main.ts",
            "polyfills": ["zone.js"],
            "tsConfig": "tsconfig.app.json",
            "assets": ["src/favicon.ico", "src/assets"],
            "styles": ["src/styles.css"],
            "budgets": [
              { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" },
              { "type": "anyComponentStyle", "maximumWarning": "4kb", "maximumError": "8kb" }
            ]
          },
          "configurations": {
            "production": {
              "outputHashing": "all",
              "optimization": true,
              "sourceMap": false
            },
            "development": {
              "optimization": false,
              "sourceMap": true
            }
          }
        }
      }
    }
  }
}

tsconfig.json — Strict TypeScript for Template Type-Checking

Angular's compiler (ngc, built on the TypeScript compiler) type-checks templates, not just .ts files — strictTemplates verifies that [value]="user.age" actually matches the declared type of age on user, and that event handlers receive the correctly typed $event. This is unique among major frameworks: a typo in a template binding is a compile-time error, not a runtime undefined.

tsconfig.jsonjson
{
  "compileOnSave": false,
  "compilerOptions": {
    "outDir": "./dist/out-tsc",
    "strict": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "skipLibCheck": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "moduleResolution": "bundler",
    "importHelpers": true,
    "target": "ES2022",
    "module": "ES2022",
    "lib": ["ES2022", "dom"]
  },
  "angularCompilerOptions": {
    "enableI18nLegacyMessageIdFormat": false,
    "strictInjectionParameters": true,
    "strictInputAccessModifiers": true,
    "strictTemplates": true
  }
}
FlagLayerWhat It Enforces
strictTemplatesangularCompilerOptionsFull type-checking of template expressions and bindings against component class members
strictInjectionParametersangularCompilerOptionsEvery constructor-injected dependency must be resolvable to a concrete provider at compile time
strictInputAccessModifiersangularCompilerOptionsPrevents binding to a component @Input() that is marked private or protected from outside the class
experimentalDecoratorscompilerOptionsRequired for @Component, @Injectable, and @Input decorator syntax used throughout Angular

Bootstrapper Entry Points: main.ts and app.config.ts

Modern Angular (17+) bootstraps via bootstrapApplication, not platformBrowserDynamic().bootstrapModule(AppModule). The root AppComponent is itself a standalone component, and application-wide providers — router configuration, HTTP client setup, error handlers — are centralized in a single ApplicationConfig object rather than scattered across a root NgModule's providers array.

src/main.tstypescript
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';

bootstrapApplication(AppComponent, appConfig).catch((err: unknown) => {
  // This is the last line of defense — if bootstrapping itself fails,
  // there is no Angular error handler yet to catch it.
  console.error('Fatal error during application bootstrap:', err);
});
src/app/app.config.tstypescript
import { ApplicationConfig, ErrorHandler } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { authTokenInterceptor } from './core/interceptors/auth-token.interceptor';
import { GlobalErrorHandler } from './core/errors/global-error-handler';

export const appConfig: ApplicationConfig = {
  providers: [
    // withComponentInputBinding lets route params bind directly to
    // component @Input() properties instead of manual ActivatedRoute reads.
    provideRouter(routes, withComponentInputBinding()),

    // withInterceptors replaces the old HTTP_INTERCEPTORS multi-provider
    // token with a plain functional array — no module registration needed.
    provideHttpClient(withInterceptors([authTokenInterceptor])),

    { provide: ErrorHandler, useClass: GlobalErrorHandler },
  ],
};

Enterprise Folder Structure: Core, Shared, Features

A three-tier folder convention keeps large Angular codebases navigable as team size grows. It is not enforced by the framework — it's a discipline the Angular community converged on because the alternative (a flat components/ and services/ folder) becomes unmanageable past roughly 30 components.

src/app/ (folder layout)text
src/app/
├── core/                    # Singleton services, app-wide guards/interceptors — imported ONCE
│   ├── interceptors/
│   │   └── auth-token.interceptor.ts
│   ├── guards/
│   │   └── auth.guard.ts
│   └── services/
│       └── session.service.ts
│
├── shared/                  # Presentational, reusable, stateless building blocks
│   ├── components/
│   │   ├── data-table/
│   │   └── status-badge/
│   └── pipes/
│       └── relative-time.pipe.ts
│
├── features/                # Business-domain-oriented, lazy-loaded feature areas
│   ├── operations-board/
│   │   ├── operations-board.routes.ts
│   │   ├── operations-board.component.ts
│   │   └── services/
│   │       └── operations.service.ts
│   └── auth/
│       ├── auth.routes.ts
│       └── login/
│           └── login.component.ts
│
├── app.component.ts
├── app.config.ts
└── app.routes.ts

The Rule Behind Each Folder

  • core/ — anything provided with providedIn: 'root' that must exist exactly once for the app's lifetime; importing core/ twice is a bug, not a feature
  • shared/ — components/pipes/directives with zero business logic and zero injected feature-specific services; a StatusBadgeComponent should work identically in five unrelated features
  • features/ — everything domain-specific, always lazy-loaded via the router (see Module 6), so a feature nobody navigates to never ships in the initial bundle

Module 1 Checkpoint

  1. Q1. Why does adding `CommonModule` to a component's TypeScript `import` statement alone not make `*ngIf` work in a standalone component's template?

  2. Q2. What does `provideRouter(routes, withComponentInputBinding())` add over a bare `provideRouter(routes)`?