RxJS & Observables – Complete Tutorial
1️⃣ What is RxJS?
RxJS (Reactive Extensions for JavaScript) is a library for handling:
It is a JavaScript library that helps us work with asynchronous data streams using Observables and operators.
In simple words, RxJS helps us handle data that arrives over time.
Real-world example
Imagine you are building a student management application.
You need to perform these operations:
- Fetch students from an API.
- Search students by name.
- Receive notifications when a student registers.
- Automatically update the student list when new data arrives.
RxJS provides tools to handle these operations efficiently.
-
Asynchronous data
Event streams
Real-time updates
API calls
User input events
👉 It works using Observables
Angular heavily uses RxJS internally (HTTP, Forms, Router, etc.)
The following expression creates a stream of marks:
of(45, 80, 65, 90, 30)The filter() operator selects marks greater than or equal to 60:
filter(marks => marks >= 60)The map() operator adds five marks to each remaining value:
map(marks => marks + 5)Finally, subscribe() receives and prints the results.
Remember
RxJS gives us three major capabilities:
- Create streams:
of(),from(),interval() - Transform streams:
map(),filter(),debounceTime() - Consume streams:
subscribe()
import { of } from 'rxjs';
import { map, filter } from 'rxjs/operators';
const marks$ = of(45, 80, 65, 90, 30);
marks$
.pipe(
filter(marks => marks >= 60),
map(marks => marks + 5)
)
.subscribe(marks => {
console.log(marks);
});
2️⃣ What is an Observable?
✅ Simple Definition
Observable is a stream of data that emits values over time
an Observable can emit:
- Zero or more values
- An error
- A completion notification
Think of an Observable as a TV channel.
The channel can broadcast several programs over time. You can watch the channel to receive them.
Similarly, an Observable can emit values, and a subscriber can receive those values.
Think like:
YouTube Live Stream 🎥
WhatsApp Messages 📩
Stock Market Updates 📈
API response
All are data streams
3️⃣ Observable vs Promise
| Feature | Observable | Promise |
|---|---|---|
| Multiple values | ✅ Yes | ❌ No |
| Cancelable | ✅ Yes | ❌ No |
| Lazy execution | ✅ Yes | ❌ No |
| Operators | ✅ Many | ❌ Limited |
👉 Angular HTTP returns Observable
4️⃣ Creating Observable
Example 1: Basic Observable
import { Observable } from 'rxjs';
const myObservable = new Observable(observer => {
observer.next("Hello");
observer.next("World");
observer.complete();
});
myObservable.subscribe(data => {
console.log(data);
});
Output:
Hello
World
5️⃣ Observable Lifecycle
-
next() → Emit value
-
error() → Emit error
-
complete() → Finish stream
6️⃣ What is Subscription?
To receive data from Observable, we must subscribe.
Important: Creating an Observable does not necessarily execute its work immediately. A typical RxJS Observable is lazy: its producer starts when someone subscribes.
myObservable.subscribe({
next: (data) => console.log(data),
error: (err) => console.log(err),
complete: () => console.log("Completed")
});3. What is subscribe()?
Definition
The
subscribe()method registers a consumer that receives values emitted by an Observable.In simple words:
Observable produces values, and subscribe() allows you to receive them.
Let's understand this with an example.
import { of } from 'rxjs'; const marks$ = of(70, 80, 90); marks$.subscribe(marks => { console.log(marks); });Output:
70 80 90Here:
of(70, 80, 90)creates an Observable.subscribe()registers a consumer.marks => console.log(marks)handles each emitted value.Understanding subscribe() with three callbacks
An Observable can emit values, report an error, or complete.
You can handle all three notifications using
subscribe().import { Observable } from 'rxjs'; const student$ = new Observable<string>(observer => { observer.next('Rahul'); observer.next('Amit'); observer.error('Something went wrong'); }); student$.subscribe({ next: (student) => { console.log('Student:', student); }, error: (err) => { console.log('Error:', err); }, complete: () => { console.log('Completed'); } });Output:
Student: Rahul Student: Amit Error: Something went wrongNotice that
Completedis not printed.Why?
Because
observer.error()terminates the stream with an error.The
completecallback is not called after an error.The three notifications
Notification Purpose nextReceives a value errorHandles an error completeHandles successful completion of the stream You can use only the callbacks you need.
For example, this is also valid:
student$.subscribe({ next: student => console.log(student), error: err => console.error(err) });7️⃣ What is Subject?
A Subject is a special type of Observable that you can also use to send values
to its subscribers.
An ordinary Observable is commonly used to produce values.
A Subject provides methods such as:
next()— send a valueerror()— terminate with an errorcomplete()— complete the streamA Subject is both:
- An Observable that consumers can subscribe to.
- An Observer that can receive values through its methods.
Let's understand this with an example.
Example 1: Basic Subject
import { Subject } from 'rxjs'; const messageSubject = new Subject<string>(); messageSubject.subscribe(message => { console.log('Subscriber 1:', message); }); messageSubject.next('Hello Rahul'); messageSubject.next('Welcome to Angular'); messageSubject.subscribe(message => { console.log('Subscriber 2:', message); }); messageSubject.next('RxJS is interesting');Why does Subscriber 2 receive only the last message?
Because a regular Subject does not replay previous values to new subscribers.
Subscriber 2 joined after the first two messages were emitted.
Therefore, it receives only the future value:
Subject = Observable + Observer
It can:
Emit values Subscribe to values Share data between components 8️⃣ Types of Subjects Subject BehaviorSubject ReplaySubject AsyncSubject You asked for: ✔ Subject ✔ BehaviorSubject ✔ AsyncPipe
🔹 1. Subject
Definition:
Subject does NOT store previous value.
If new subscriber joins, it won’t get old values.
Example:
import { Subject } from 'rxjs';
const subject = new Subject();
subject.subscribe(data => console.log("User 1:", data));
subject.next("Hello");
subject.subscribe(data => console.log("User 2:", data));
subject.next("World");Output:
User 1: Hello
User 1: World
User 2: World👉 User 2 did NOT receive "Hello"
🔹2. BehaviorSubject
Definition:
BehaviorSubject always stores the latest value.
New subscriber immediately receives last value.
Must pass initial value
import { BehaviorSubject } from 'rxjs';
const behavior = new BehaviorSubject("Initial Value");
behavior.subscribe(data => console.log("User 1:", data));
behavior.next("Hello");
behavior.subscribe(data => console.log("User 2:", data));Output:
User 1: Initial Value
User 1: Hello
User 2: Hello👉 User 2 automatically got latest value
Real Angular Example (Best Practice)
Scenario: Share data between components
Step 1: Create Service
@Injectable({ providedIn: 'root' })
export class DataService {
private messageSource = new BehaviorSubject<string>('Default Message');
currentMessage$ = this.messageSource.asObservable();
changeMessage(message: string) {
this.messageSource.next(message);
}
}Step 2: Sender Component
constructor(private dataService: DataService) {}
sendMessage() {
this.dataService.changeMessage("Hello from Component A");
}Step 3: Receiver Component
message: string = '';
constructor(private dataService: DataService) {}
ngOnInit() {
this.dataService.currentMessage$.subscribe(msg => {
this.message = msg;
});
}Class room assignments:Create Project to demonstrate all this :Let's build a complete Student Management Application using Angular 21 and RxJS that demonstrates all four real-world operations:
- Fetch Students from an API — using
Observableandsubscribe().- Search Students by Name — using
Subject,debounceTime(), anddistinctUntilChanged().- Receive Notifications When a Student Registers — using
Subjectto broadcast events.- Automatically Update the Student List — using RxJS streams to update the UI when new students register.
We will use two components, one service, and a mock API so you can run the example without creating a backend.
1. Understand the Application
Imagine you are developing a student management system for a training institute.
The application has two components:
- StudentListComponent: Displays and searches students.
- RegisterStudentComponent: Registers new students.
Both components communicate through a shared
StudentService.How it works
StudentService | ----------------------- | | v v StudentListComponent RegisterStudentComponent | | v v Fetch Students Register Student | v Search Students | v Display Updated List | v Receive NotificationsFor this example, we will use an in-memory data source to simulate an API. Later, you can replace it with your actual ASP.NET Core Web API.
2. Create the Angular Project
Open your terminal and execute:
ng new student-management --standalone --routing=false --style=cssNavigate to the project:
cd student-managementGenerate the service:
ng generate service services/studentGenerate the two components:
ng generate component components/student-list ng generate component components/register-studentOur project structure will look like this:
src/ └── app/ ├── components/ │ ├── student-list/ │ │ ├── student-list.ts │ │ ├── student-list.html │ │ └── student-list.css │ │ │ └── register-student/ │ ├── register-student.ts │ ├── register-student.html │ └── register-student.css │ ├── services/ │ └── student.ts │ ├── app.ts └── app.htmlNote: Angular CLI filenames can vary according to the project's configuration. Use the equivalent generated files if your filenames differ.
3. Create the Student Model
First, define what a student looks like.
Create
src/app/models/student.ts.export interface Student { id: number; name: string; course: string; email: string; }Every student will have four properties:
Property Data type Example idnumber 1 namestring Rahul Sharma coursestring Angular string rahul@example.com This interface ensures that our student data follows a consistent structure.
4. Create the Student Service
This is the most important part of our application.
The service will manage:
- Fetching students.
- Searching students.
- Registering new students.
- Broadcasting registration notifications.
- Automatically updating the student list.
Open
src/app/services/student.ts.import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable, Subject, of, delay } from 'rxjs'; import { Student } from '../models/student'; @Injectable({ providedIn: 'root' }) export class StudentService { // Initial student data private students: Student[] = [ { id: 1, name: 'Rahul Sharma', course: 'Angular', email: 'rahul@example.com' }, { id: 2, name: 'Priya Verma', course: 'React', email: 'priya@example.com' }, { id: 3, name: 'Amit Patel', course: '.NET', email: 'amit@example.com' }, { id: 4, name: 'Neha Singh', course: 'Python', email: 'neha@example.com' } ]; // 1. Observable data stream for the student list private studentSubject = new BehaviorSubject<Student[]>(this.students); studentList$ = this.studentSubject.asObservable(); // 2. Subject for search text private searchSubject = new Subject<string>(); searchText$ = this.searchSubject.asObservable(); // 3. Subject for registration notifications private notificationSubject = new Subject<string>(); notifications$ = this.notificationSubject.asObservable(); // Fetch students from a simulated API getStudents(): Observable<Student[]> { return of(this.students).pipe( delay(500) ); } // Search students searchStudents(searchText: string): void { this.searchSubject.next(searchText); } // Register a new student registerStudent(student: Student): void { this.students = [ ...this.students, student ]; // Broadcast the updated student list this.studentSubject.next(this.students); // Send a registration notification this.notificationSubject.next( `${student.name} registered successfully!` ); } // Get the next student ID getNextId(): number { return this.students.length > 0 ? Math.max(...this.students.map(s => s.id)) + 1 : 1; } }Understanding the Service
A. What is
Observable?Consider this method:
getStudents(): Observable<Student[]> { return of(this.students).pipe( delay(500) ); }
Observable<Student[]>means that the observable emits an array of students.Here,
of()creates an observable from the student array, anddelay(500)simulates a short API response delay.Important: This is a simulated API, not an actual HTTP request.
An actual API request would use Angular's
HttpClient:getStudents(): Observable<Student[]> { return this.http.get<Student[]>( 'https://localhost:7001/api/students' ); }For this actual HTTP version, inject
HttpClientinto the service and configureprovideHttpClient()in your application providers.B. What is
BehaviorSubject?private studentSubject = new BehaviorSubject<Student[]>(this.students);A
BehaviorSubjectstores its latest value and immediately emits that value to new subscribers.When a student registers:
this.studentSubject.next(this.students);The service broadcasts the updated array to all subscribers.
We expose it as an observable:
studentList$ = this.studentSubject.asObservable();This allows components to receive updates without directly modifying the subject.
C. What is
Subject?We use two subjects for different purposes.
Search Subject:
private searchSubject = new Subject<string>();When the user types something:
searchStudents(searchText: string): void { this.searchSubject.next(searchText); }The
.next()method emits the search text to subscribers.Notification Subject:
private notificationSubject = new Subject<string>();When a student registers:
this.notificationSubject.next( `${student.name} registered successfully!` );Any component subscribed to
notifications$can receive the message.Unlike a
BehaviorSubject, a regularSubjectdoes not replay its last value to new subscribers.5. Create the Student Registration Component
This component allows users to register new students.
Open
src/app/components/register-student/register-student.ts.import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { StudentService } from '../../services/student'; import { Student } from '../../models/student'; @Component({ selector: 'app-register-student', standalone: true, imports: [FormsModule], templateUrl: './register-student.html', styleUrl: './register-student.css' }) export class RegisterStudentComponent { name = ''; course = ''; email = ''; constructor( private studentService: StudentService ) {} registerStudent(): void { if ( !this.name.trim() || !this.course.trim() || !this.email.trim() ) { alert('Please enter all student details.'); return; } const student: Student = { id: this.studentService.getNextId(), name: this.name.trim(), course: this.course.trim(), email: this.email.trim() }; // Register student through the service this.studentService.registerStudent(student); // Reset form this.name = ''; this.course = ''; this.email = ''; } }Registration HTML
Open
src/app/components/register-student/register-student.html.<div class="card"> <h2>Register New Student</h2> <form (ngSubmit)="registerStudent()"> <div class="form-group"> <label>Student Name</label> <input type="text" name="name" [(ngModel)]="name" placeholder="Enter student name" required> </div> <div class="form-group"> <label>Course</label> <select name="course" [(ngModel)]="course" required> <option value="">Select Course</option> <option value="Angular">Angular</option> <option value="React">React</option> <option value=".NET">.NET</option> <option value="Python">Python</option> <option value="Java">Java</option> </select> </div> <div class="form-group"> <label>Email</label> <input type="email" name="email" [(ngModel)]="email" placeholder="Enter email" required> </div> <button type="submit"> Register Student </button> </form> </div>Registration CSS
Open
src/app/components/register-student/register-student.css..card { background: white; padding: 24px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); } h2 { margin-top: 0; color: #1d4ed8; } .form-group { margin-bottom: 16px; } label { display: block; margin-bottom: 6px; font-weight: 600; } input, select { width: 100%; box-sizing: border-box; padding: 11px; border: 1px solid #d1d5db; border-radius: 7px; font-size: 15px; } button { background: #2563eb; color: white; border: none; border-radius: 7px; padding: 12px 18px; cursor: pointer; font-weight: 600; } button:hover { background: #1d4ed8; }What happens when the user registers?
Suppose the user enters:
Name: Suresh Kumar Course: Angular Email: suresh@example.comWhen the user clicks Register Student, this method executes:
this.studentService.registerStudent(student);Inside the service, two important operations occur:
this.studentSubject.next(this.students);This broadcasts the updated student list.
this.notificationSubject.next( `${student.name} registered successfully!` );This broadcasts a notification.
We will now subscribe to both streams in the student list component.
6. Create the Student List Component
This component will:
- Fetch the initial student list.
- Search students by name.
- Listen for new student registrations.
- Automatically refresh the list.
- Display notifications.
Open
src/app/components/student-list/student-list.ts.import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Subscription, debounceTime, distinctUntilChanged, startWith, map } from 'rxjs'; import { Student } from '../../models/student'; import { StudentService } from '../../services/student'; @Component({ selector: 'app-student-list', standalone: true, imports: [CommonModule], templateUrl: './student-list.html', styleUrl: './student-list.css' }) export class StudentListComponent implements OnInit { students: Student[] = []; filteredStudents: Student[] = []; searchText = ''; notification = ''; private subscriptions = new Subscription(); constructor( private studentService: StudentService ) {} ngOnInit(): void { // 1. Fetch initial students const fetchSubscription = this.studentService.getStudents().subscribe({ next: (data) => { this.students = data; this.filteredStudents = data; console.log('Students fetched:', data); }, error: (error) => { console.error( 'Error fetching students:', error ); } }); this.subscriptions.add(fetchSubscription); // 2. Automatically receive updated student lists const listSubscription = this.studentService.studentList$.subscribe( (data) => { this.students = data; this.applySearch(); } ); this.subscriptions.add(listSubscription); // 3. Listen for search text const searchSubscription = this.studentService.searchText$ .pipe( debounceTime(300), distinctUntilChanged(), startWith(''), map(text => text.trim().toLowerCase()) ) .subscribe((searchText) => { this.searchText = searchText; this.applySearch(); }); this.subscriptions.add(searchSubscription); // 4. Listen for registration notifications const notificationSubscription = this.studentService.notifications$.subscribe( (message) => { this.notification = message; } ); this.subscriptions.add(notificationSubscription); } // Filter students by name applySearch(): void { const search = this.searchText.trim().toLowerCase(); this.filteredStudents = this.students.filter( student => student.name.toLowerCase().includes(search) ); } // Send search text to the service onSearch(event: Event): void { const input = event.target as HTMLInputElement; this.studentService.searchStudents(input.value); } ngOnDestroy(): void { this.subscriptions.unsubscribe(); } }Understanding
subscribe()Consider this code:
this.studentService.getStudents().subscribe({ next: (data) => { this.students = data; }, error: (error) => { console.error(error); } });The
subscribe()method starts the observable execution and registers callbacks for its emissions.
next: Handles emitted student data.error: Handles an error if the observable fails.complete: Can handle completion if needed.For our simulated API, the observable emits the student array after 500 milliseconds and completes.
Important distinction:
subscribe()does not itself fetch data. It subscribes to the observable; the work performed depends on how that observable was created.Understanding
debounceTime()debounceTime(300)Suppose the user types:
R Ra Rah RahulInstead of processing every keystroke immediately,
debounceTime(300)waits until the user stops typing for 300 milliseconds before forwarding the latest search value.This is useful for search boxes and API calls.
Understanding
distinctUntilChanged()distinctUntilChanged()This operator prevents consecutive identical values from being emitted downstream.
For example:
Rahul Rahul RahulOnly the first consecutive
Rahulpasses through.If the value changes to
Rahul S, that new value passes through.Understanding
startWith()startWith('')This operator emits an initial empty search string when the stream is subscribed to.
That allows the application to initially display all students.
Understanding
map()map(text => text.trim().toLowerCase())This transforms the incoming search text before filtering.
For example:
" RAHUL " → "rahul"This makes the search case-insensitive and ignores surrounding whitespace.
7. Create the Student List HTML
Open
src/app/components/student-list/student-list.html.<div class="card"> <h2>Student Management System</h2> <p> Total Registered Students: <strong>{{ students.length }}</strong> </p> <!-- Registration Notification --> <div class="notification" *ngIf="notification"> {{ notification }} </div> <!-- Search Students --> <div class="search-box"> <input type="text" placeholder="Search students by name..." (input)="onSearch($event)"> </div> <!-- Student Table --> <div class="table-container"> <table> <thead> <tr> <th>ID</th> <th>Name</th> <th>Course</th> <th>Email</th> </tr> </thead> <tbody> <tr *ngFor="let student of filteredStudents"> <td>{{ student.id }}</td> <td>{{ student.name }}</td> <td>{{ student.course }}</td> <td>{{ student.email }}</td> </tr> <tr *ngIf="filteredStudents.length === 0"> <td colspan="4" class="empty"> No students found. </td> </tr> </tbody> </table> </div> </div>Student List CSS
Open
src/app/components/student-list/student-list.css..card { background: white; padding: 24px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); margin-bottom: 24px; } h2 { color: #1d4ed8; margin-top: 0; } .search-box { margin: 20px 0; } .search-box input { width: 100%; box-sizing: border-box; padding: 12px; border: 1px solid #d1d5db; border-radius: 8px; font-size: 15px; } .table-container { overflow-x: auto; } table { width: 100%; border-collapse: collapse; } th, td { text-align: left; padding: 13px; border-bottom: 1px solid #e5e7eb; } th { background: #eff6ff; color: #1d4ed8; } .notification { background: #dcfce7; color: #166534; border: 1px solid #86efac; padding: 12px; border-radius: 8px; margin: 16px 0; } .empty { text-align: center; padding: 25px; color: #6b7280; }8. Display Both Components in the Main App
Open
src/app/app.ts.import { Component } from '@angular/core'; import { StudentListComponent } from './components/student-list/student-list'; import { RegisterStudentComponent } from './components/register-student/register-student'; @Component({ selector: 'app-root', standalone: true, imports: [ StudentListComponent, RegisterStudentComponent ], templateUrl: './app.html', styleUrl: './app.css' }) export class AppComponent { title = 'Student Management System'; }Open
src/app/app.html.<div class="container"> <header> <h1>Student Management System</h1> <p> Angular 21 + RxJS Real-World Example </p> </header> <app-student-list></app-student-list> <app-register-student></app-register-student> </div>Open
src/app/app.css.:host { display: block; min-height: 100vh; background: #f3f6fb; } .container { max-width: 1100px; margin: 0 auto; padding: 30px 20px; } header { text-align: center; margin-bottom: 30px; } header h1 { color: #1e3a8a; margin-bottom: 8px; } header p { color: #64748b; }Make sure your application's bootstrap configuration is set up for standalone components, as it is in a standard modern Angular project.
9. Run the Application
Execute:
ng serveOpen the local development URL shown in your terminal, usually
http://localhost:4200.You should see:
- Four initially registered students.
- A search input.
- A registration form.
- A student count.
- A notification area.
Test 1: Fetch Students
When the application starts,
ngOnInit()calls:this.studentService.getStudents().subscribe({ next: (data) => { this.students = data; this.filteredStudents = data; } });After the simulated 500 ms delay, the initial four students are displayed.
Test 2: Search Students
Enter:
RahulThe list displays only:
Rahul SharmaNow enter:
PriyaThe list displays only:
Priya VermaSearch is case-insensitive, so
rahul,RAHUL, andRahulproduce the same result.Test 3: Register a New Student
Enter:
Name: Suresh Kumar Course: Angular Email: suresh@example.comClick Register Student.
The service executes:
this.students = [ ...this.students, student ]; this.studentSubject.next(this.students);The
BehaviorSubjectemits the new array.The student list component receives the new array and updates the table automatically.
The student count changes from four to five.
The notification also displays:
Suresh Kumar registered successfully!No page refresh is required.
One important detail: If a search term is active, the newly registered student appears in the filtered list only if their name matches that search term.
10. Understand the Complete RxJS Flow
Let's connect the concepts.
Flow A: Fetching Students
StudentService.getStudents() | v Observable | v subscribe() | v StudentListComponent | v Display StudentsHere, the observable emits the initial student data.
Flow B: Searching Students
User Types Name | v onSearch() | v Subject.next() | v searchText$ | v debounceTime(300) | v distinctUntilChanged() | v map() | v subscribe() | v applySearch() | v Display Matching StudentsThis flow separates the search input from the code that processes search values.
Flow C: Registering Students
RegisterStudentComponent | v registerStudent(student) | v Update Student Array | v studentSubject.next() | v BehaviorSubject Emits | v StudentListComponent | v Update Student ListFlow D: Registration Notifications
RegisterStudentComponent | v StudentService | v notificationSubject.next() | v notifications$ | v subscribe() | v Display Notification11. Why Use Different RxJS Subjects?
This example uses two kinds of subjects and an observable stream.
Feature ObservableSubjectBehaviorSubjectMain purpose Represent an asynchronous data stream Emit values to subscribers Emit values while retaining the latest value Stores the latest value Not inherently No Yes Replays the latest value to new subscribers Depends on the observable No Yes Used in our example Fetch students Search and notifications Student list updates A simple way to explain this to students:
- Observable: A stream of data that you can subscribe to.
- Subscribe: Register to receive values from that stream.
- Subject: A stream that also lets you push values using
.next().- BehaviorSubject: A subject that remembers its latest value and emits it immediately to new subscribers.
Angular 21 HTTP Interceptors — Explained with a Practical Example
In Angular, an HTTP Interceptor is used to intercept HTTP requests before they reach the server and HTTP responses before they reach your application code.
Interceptors are commonly used for:
- Adding JWT authentication tokens to API requests.
- Handling HTTP errors globally.
- Logging requests and responses.
- Showing a loader while API calls are running.
- Adding common headers to every request.
Let's understand this with a simple example.
1. What is an HTTP Interceptor?
Imagine you are building a student management application.
Your Angular application calls these APIs:
GET /api/students POST /api/students PUT /api/students/1 DELETE /api/students/1Suppose every API request requires an authentication token.
Without an interceptor, you might have to add the token manually to every request.
this.http.get('/api/students', { headers: { Authorization: 'Bearer YOUR_TOKEN' } });You would have to repeat this logic for multiple API calls.
An interceptor solves this problem by adding the token automatically to outgoing HTTP requests.
How does an interceptor work?
The request flow looks like this:
Angular Component | v HttpClient | v HTTP Interceptor | | Add Authorization Token v API Server | v HTTP Interceptor | v Angular ComponentThe interceptor can modify outgoing requests and inspect incoming responses.
2. Create a Simple Angular 21 HTTP Interceptor
We will create an interceptor that automatically adds a custom header to every HTTP request.
Step 1: Generate an interceptor
Run this command:
ng generate interceptor interceptors/authAngular CLI will generate an interceptor file. Depending on your project's conventions, its name might be
auth.interceptor.ts.For Angular 21, we'll use a functional interceptor, which is a modern and straightforward approach.
Step 2: Write the interceptor code
Open
auth.interceptor.tsand write:import { HttpInterceptorFn } from '@angular/common/http'; export const authInterceptor: HttpInterceptorFn = (req, next) => { console.log('Request URL:', req.url); const token = 'my-demo-token'; const modifiedRequest = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }); return next(modifiedRequest); };Let's understand each part.
1.
HttpInterceptorFnexport const authInterceptor: HttpInterceptorFn = (req, next) => {This defines a functional HTTP interceptor.
It receives two important parameters:
req: The outgoing HTTP request.next: The next handler in the interceptor chain, which eventually forwards the request toward the server.2. Read the token
const token = 'my-demo-token';This is a demonstration token.
In a real application, you would retrieve the authentication token from your authentication service or another appropriate secure token store.
3. Clone the request
const modifiedRequest = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } });Angular HTTP requests are immutable. You cannot directly change the original request's headers.
Instead, you create a modified copy using
clone().The resulting request contains this header:
Authorization: Bearer my-demo-token4. Forward the modified request
return next(modifiedRequest);This passes the modified request to the next handler in the chain.
If there are no more interceptors, the request proceeds to the HTTP backend.
Important: An interceptor must return the Observable from
next()or another appropriate Observable representing the HTTP operation.3. Register the Interceptor in Angular 21
Creating an interceptor is not enough. You must register it with Angular's HTTP client.
Open
app.config.ts.Add
provideHttpClient()andwithInterceptors().import { ApplicationConfig } from '@angular/core'; import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { authInterceptor } from './interceptors/auth.interceptor'; export const appConfig: ApplicationConfig = { providers: [ provideHttpClient( withInterceptors([ authInterceptor ]) ) ] };Why do we use
withInterceptors()?withInterceptors([ authInterceptor ])This registers the interceptor with Angular's HTTP client.
Whenever a component or service makes a request through this configured
HttpClient, Angular applies the interceptor.Note: If your application already calls
provideHttpClient(), update the existing configuration rather than registering another independent HTTP client configuration unnecessarily.4. Test the Interceptor Using a Component
Let's create a simple component that fetches students.
Step 1: Create the component
ng generate component studentStep 2: Write the component code
Open
student.tsor the corresponding generated TypeScript file.import { Component, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; @Component({ selector: 'app-student', standalone: true, template: ` <h2>Student Management</h2> <button (click)="getStudents()"> Load Students </button> <pre>{{ students | json }}</pre> `, imports: [] }) export class StudentComponent { private http = inject(HttpClient); students: unknown = []; getStudents(): void { this.http.get('/api/students').subscribe({ next: (data) => { this.students = data; }, error: (error) => { console.error('API Error:', error); } }); } }For this example, assume
/api/studentsis an endpoint implemented by your backend. Replace it with your actual API URL.If you are using the
jsonpipe as shown, importJsonPipefrom@angular/commonand add it to the component'simportsarray.What happens when you click Load Students?
Your component executes:
this.http.get('/api/students')The interceptor automatically modifies the request before it is sent.
The outgoing HTTP request contains:
GET /api/students Authorization: Bearer my-demo-tokenYou do not need to add the header inside the component.
That is the main advantage of an interceptor: common HTTP logic stays in one place.
5. Example 2: Handle HTTP Errors Globally
Interceptors are not limited to authentication. They can also handle errors.
Suppose your backend returns:
401 UnauthorizedThis often indicates that authentication is missing or invalid.
Instead of repeating error-handling logic in every component, you can create an error interceptor.
Create
error.interceptor.ts:import { HttpInterceptorFn } from '@angular/common/http'; import { catchError, throwError } from 'rxjs'; export const errorInterceptor: HttpInterceptorFn = (req, next) => { return next(req).pipe( catchError(error => { if (error.status === 401) { console.error('Unauthorized! Please log in.'); } if (error.status === 403) { console.error('You do not have permission.'); } if (error.status === 500) { console.error('Internal server error.'); } return throwError(() => error); }) ); };Understand
catchError()catchError(error => {This operator handles errors emitted by the HTTP request Observable.
For example:
if (error.status === 401) { console.error('Unauthorized! Please log in.'); }This identifies an unauthorized response.
Then:
return throwError(() => error);This re-emits the error so the component or calling service can also handle it.
Important: This interceptor logs errors but does not automatically redirect the
user to the login page. You can add that behavior using Angular's Router and
an appropriate authentication flow.
Register both interceptors
Update
app.config.ts:import { ApplicationConfig } from '@angular/core'; import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { authInterceptor } from './interceptors/auth.interceptor'; import { errorInterceptor } from './interceptors/error.interceptor'; export const appConfig: ApplicationConfig = { providers: [ provideHttpClient( withInterceptors([ authInterceptor, errorInterceptor ]) ) ] };The request passes through the authentication interceptor and then the error interceptor.
On the response path, the response travels back through the interceptor chain in reverse order.
6. Example 3: Add a Loader Using an Interceptor
Suppose you want to show a loading indicator whenever an API request is in progress.
You could manage loading separately in every component, but a shared interceptor
can centralize this behavior.
A basic example:
import { HttpInterceptorFn } from '@angular/common/http'; import { finalize } from 'rxjs'; export const loaderInterceptor: HttpInterceptorFn = (req, next) => { console.log('API request started'); return next(req).pipe( finalize(() => { console.log('API request completed'); }) ); };Here,
finalize()runs when the request Observable completes, errors, or is unsubscribed.This example only logs messages. To display a real application-wide loader,
you would typically use a shared loading service and account for concurrent
requests so that one completed request does not hide the loader while another
request remains active.
7. HTTP Interceptor vs HTTP Service
This is an important interview question.
Feature HTTP Service HTTP Interceptor Main purpose Implements API-related business operations Handles common HTTP request/response behavior Example Fetch students Add an authentication token Called directly by a component? Often yes, through dependency injection Usually invoked automatically by HttpClientHandles common headers? Possible Ideal for shared headers Handles global HTTP errors? Possible Useful for centralized error handling Example
A student service might contain:
getStudents() { return this.http.get('/api/students'); }An interceptor might add:
Authorization: Bearer YOUR_TOKENThe service decides which API to call.
The interceptor handles common processing for HTTP requests and responses.

0 تعليقات
POST Answer of Questions and ASK to Doubt