What is Angular?
Angular is a complete application framework maintained by Google. Where React is a library you assemble a stack around, Angular ships the stack: components, routing, forms, HTTP, dependency injection, testing and a powerful CLI, all designed to work together. It is TypeScript-first and opinionated, which is exactly why large teams pick it.
That completeness is the core trade-off. You accept a steeper learning curve and more framework conventions in exchange for consistency, strong tooling and fewer architectural decisions to make from scratch. For a small prototype that can feel heavy; for a five-year enterprise codebase it is often a relief.
Components and templates
A component is a TypeScript class decorated with @Component, paired with an HTML template and optional styles.
// user-card.component.ts
import { Component, Input } from "@angular/core";
@Component({
selector: "app-user-card",
standalone: true,
template: `
<article class="card">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</article>
`,
})
export class UserCardComponent {
@Input({ required: true }) user!: { name: string; email: string };
}
The template uses {{ }} for interpolation and directives for logic. Modern Angular uses built-in control flow blocks instead of the older structural directives.
<!-- list.html -->
@if (users.length > 0) {
<ul>
@for (user of users; track user.id) {
<li>{{ user.name }}</li>
}
</ul>
} @else {
<p>No users yet.</p>
}
The track expression in @for plays the same role as a key in React or Vue: it tells Angular how to match items between updates.
Data binding
Angular templates support four kinds of binding.
- Interpolation
{{ value }}renders a value as text. - Property binding
[src]="url"sets an element property from an expression. - Event binding
(click)="save()"calls a method when an event fires. - Two-way binding
[(ngModel)]="name"combines both for form inputs.
<!-- bindings.html -->
<img [src]="user.avatar" [alt]="user.name" />
<button (click)="save()">Save</button>
<input [(ngModel)]="query" />
Two-way binding is convenient for simple forms, but for anything with validation, reactive forms are usually a better fit.
Signals
Signals are Angular’s modern reactivity primitive. A signal holds a value and notifies interested consumers when it changes, which lets the framework update only the parts of the view that depend on it.
// cart.component.ts
import { Component, signal, computed } from "@angular/core";
@Component({
selector: "app-cart",
standalone: true,
template: `<p>Total: {{ total() }}</p>`,
})
export class CartComponent {
items = signal([{ price: 10 }, { price: 20 }]);
total = computed(() =>
this.items().reduce((sum, i) => sum + i.price, 0),
);
add(price: number) {
this.items.update((items) => [...items, { price }]);
}
}
computed derives a value from other signals and caches it until a dependency changes. effect runs a side effect when signals it reads change. Together they cover most of what components need.
RxJS and observables
Angular has deep RxJS integration. An observable represents a stream of values over time, and operators let you transform and combine those streams.
// search.component.ts
import { Component } from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";
import { debounceTime, distinctUntilChanged, switchMap } from "rxjs";
@Component({ /* ... */ })
export class SearchComponent {
private query$ = new Subject<string>();
results = toSignal(
this.query$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap((q) => this.api.search(q)),
),
{ initialValue: [] },
);
}
This is where RxJS earns its place: debouncing, cancelling stale requests and combining streams are hard to express with plain promises. For simple synchronous state, signals are simpler. Modern Angular lets you mix both and convert with toSignal and toObservable.
Services and dependency injection
Shared logic and data belong in services, not components. Dependency injection supplies them wherever they are needed.
// users.service.ts
import { Injectable, inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
@Injectable({ providedIn: "root" })
export class UsersService {
private http = inject(HttpClient);
getAll() {
return this.http.get<User[]>("/api/users");
}
}
// users.component.ts
export class UsersComponent {
private users = inject(UsersService);
list = toSignal(this.users.getAll(), { initialValue: [] });
}
providedIn: "root" registers a singleton for the whole app. DI makes services easy to swap in tests and keeps components focused on presentation.
Routing
The Angular Router maps URLs to components and supports nested routes, guards, resolvers and lazy loading.
// app.routes.ts
import { Routes } from "@angular/router";
export const routes: Routes = [
{ path: "", component: HomeComponent },
{ path: "users/:id", component: UserProfileComponent },
{
path: "admin",
loadChildren: () => import("./admin/admin.routes").then((m) => m.ADMIN_ROUTES),
},
];
Lazy loading feature areas keeps the initial bundle small, and guards let you block navigation until a condition such as authentication is met.
Forms and HTTP
Angular has two form approaches. Reactive forms define the model and validators in the class, which scales well and is testable. Template-driven forms use directives like ngModel and are quicker for simple cases.
// signup.component.ts
form = new FormGroup({
email: new FormControl("", [Validators.required, Validators.email]),
password: new FormControl("", [Validators.required, Validators.minLength(8)]),
});
submit() {
if (this.form.invalid) return;
this.http.post("/api/signup", this.form.getRawValue()).subscribe();
}
HttpClient returns observables, so requests compose with RxJS operators for retries, cancellation and error handling.
Best practices
- Prefer standalone components and signals for new code.
- Keep components focused on presentation and move logic into services.
- Use reactive forms for anything with validation or dynamic fields.
- Lazy-load feature areas to keep the initial bundle small.
- Use
OnPushchange detection or signals to limit unnecessary checks. - Track items in
@forwith a stable unique value. - Unsubscribe from long-lived observables, or use the async pipe and
toSignal.
Common mistakes
- Putting business logic in components instead of services.
- Subscribing manually and forgetting to unsubscribe, causing leaks.
- Reaching for RxJS when a signal would be simpler.
- Mutating a signal’s value directly instead of using
setorupdate. - Building one enormous NgModule instead of lazy-loaded feature routes.
- Treating Angular like React and fighting the framework’s structure.
Where to go next
Angular rewards patience with a structure that holds up over years. Deepen your TypeScript, understand how it compares to React and Vue, and learn the Node.js tooling that powers its CLI. Then build a feature with routing, a service and a reactive form to see how the pieces fit.