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
importsarray) controls what a template can reference — a component can be exported from its.tsfile 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.
# 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 --openangular.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.
{
"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.
{
"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
}
}| Flag | Layer | What It Enforces |
|---|---|---|
| strictTemplates | angularCompilerOptions | Full type-checking of template expressions and bindings against component class members |
| strictInjectionParameters | angularCompilerOptions | Every constructor-injected dependency must be resolvable to a concrete provider at compile time |
| strictInputAccessModifiers | angularCompilerOptions | Prevents binding to a component @Input() that is marked private or protected from outside the class |
| experimentalDecorators | compilerOptions | Required 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.
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);
});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/
├── 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.tsThe 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
StatusBadgeComponentshould 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
Q1. Why does adding `CommonModule` to a component's TypeScript `import` statement alone not make `*ngIf` work in a standalone component's template?
Q2. What does `provideRouter(routes, withComponentInputBinding())` add over a bare `provideRouter(routes)`?