Ad Code

✨🎆 JOIN MERN, JAVA, PYTHON, AI, DEVOPS, SALESFORCE Courses 🎆✨

Get 100% Placement Oriented Program CLICK to new more info click

RxJS & Observables – Complete Tutorial

 

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:

  1. Fetch students from an API.
  2. Search students by name.
  3. Receive notifications when a student registers.
  4. 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

FeatureObservablePromise
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

  1. next() → Emit value

  2. error() → Emit error

  3. 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
90

Here:

  • 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 wrong

Notice that Completed is not printed.

Why?

Because observer.error() terminates the stream with an error.

The complete callback is not called after an error.

The three notifications

NotificationPurpose
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 value
  • error() — terminate with an error
  • complete() — complete the stream

A Subject is both:

  1. An Observable that consumers can subscribe to.
  2. 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:

  1. Fetch Students from an API — using Observable and subscribe().
  2. Search Students by Name — using Subject, debounceTime(), and distinctUntilChanged().
  3. Receive Notifications When a Student Registers — using Subject to broadcast events.
  4. 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 Notifications

For 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=css

Navigate to the project:

cd student-management

Generate the service:

ng generate service services/student

Generate the two components:

ng generate component components/student-list
ng generate component components/register-student

Our 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.html

Note: 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:

PropertyData typeExample
idnumber1
namestringRahul Sharma
coursestringAngular
emailstringrahul@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, and delay(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 HttpClient into the service and configure provideHttpClient() in your application providers.

B. What is BehaviorSubject?

private studentSubject =
  new BehaviorSubject<Student[]>(this.students);

A BehaviorSubject stores 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 regular Subject does 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.com

When 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:

  1. Fetch the initial student list.
  2. Search students by name.
  3. Listen for new student registrations.
  4. Automatically refresh the list.
  5. 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
Rahul

Instead 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
Rahul

Only the first consecutive Rahul passes 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 serve

Open 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:

Rahul

The list displays only:

Rahul Sharma

Now enter:

Priya

The list displays only:

Priya Verma

Search is case-insensitive, so rahul, RAHUL, and Rahul produce the same result.

Test 3: Register a New Student

Enter:

Name: Suresh Kumar
Course: Angular
Email: suresh@example.com

Click Register Student.

The service executes:

this.students = [
  ...this.students,
  student
];

this.studentSubject.next(this.students);

The BehaviorSubject emits 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 Students

Here, 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 Students

This 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 List

Flow D: Registration Notifications

RegisterStudentComponent
          |
          v
  StudentService
          |
          v
notificationSubject.next()
          |
          v
    notifications$
          |
          v
       subscribe()
          |
          v
 Display Notification

11. Why Use Different RxJS Subjects?

This example uses two kinds of subjects and an observable stream.

FeatureObservableSubjectBehaviorSubject
Main purposeRepresent an asynchronous data streamEmit values to subscribersEmit values while retaining the latest value
Stores the latest valueNot inherentlyNoYes
Replays the latest value to new subscribersDepends on the observableNoYes
Used in our exampleFetch studentsSearch and notificationsStudent 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/1

Suppose 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 Component

The 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/auth

Angular 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.ts and 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. HttpInterceptorFn

export 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-token

4. 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() and withInterceptors().

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 student

Step 2: Write the component code

Open student.ts or 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/students is an endpoint implemented by your backend. Replace it with your actual API URL.

If you are using the json pipe as shown, import JsonPipe from @angular/common and add it to the component's imports array.

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-token

You 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 Unauthorized

This 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.

FeatureHTTP ServiceHTTP Interceptor
Main purposeImplements API-related business operationsHandles common HTTP request/response behavior
ExampleFetch studentsAdd an authentication token
Called directly by a component?Often yes, through dependency injectionUsually invoked automatically by HttpClient
Handles common headers?PossibleIdeal for shared headers
Handles global HTTP errors?PossibleUseful for centralized error handling

Example

A student service might contain:

getStudents() {
  return this.http.get('/api/students');
}

An interceptor might add:

Authorization: Bearer YOUR_TOKEN

The service decides which API to call.

The interceptor handles common processing for HTTP requests and responses.



إرسال تعليق

0 تعليقات