// app.module.ts
import { HttpClientModule } from '@angular/common/http';
@NgModule({
imports: [BrowserModule, HttpClientModule],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule {}
// For standalone apps (Angular 15+)
// main.ts
import { provideHttpClient } from '@angular/common/http';
bootstrapApplication(AppComponent, {
providers: [provideHttpClient()]
});
HttpClientModule should be imported only once in the root module. It provides HttpClient, which uses Observables for all HTTP operations and includes features like interceptors, typed responses, and progress events.
Why it matters: Tests fundamental knowledge of HTTP setup in Angular. Interviewers want to confirm you understand module imports and the singleton nature of HttpClient across applications.
Real applications: Any app making API calls needs HttpClientModule imported in AppModule or provided via provideHttpClient() in standalone apps. This is the gateway to all backend communication.
Common mistakes: Developers forget to import HttpClientModule entirely, then get cryptic errors about HttpClient not being provided. They import it multiple times in feature modules instead of just the root.
@Injectable({ providedIn: 'root' })
export class ProductService {
private apiUrl = '/api/products';
constructor(private http: HttpClient) {}
// Typed GET request
getProducts(): Observable<Product[]> {
return this.http.get<Product[]>(this.apiUrl);
}
getProductById(id: number): Observable<Product> {
return this.http.get<Product>(this.apiUrl + '/' + id);
}
}
// Component usage
export class ProductListComponent {
products$ = this.productService.getProducts();
constructor(private productService: ProductService) {}
}
HttpClient automatically parses JSON responses. Use generic type parameters like get<Product[]> for type safety. The request is not sent until something subscribes to the Observable.
Why it matters: Tests understanding of Observables and lazy evaluation in Angular. Shows you know HttpClient doesn't execute requests until subscription.
Real applications: Every data-fetching service uses get requests typed with generics for compile-time safety. Combining with services ensures type-safe API contracts throughout the application.
Common mistakes: Developers forget to subscribe, wondering why the request never fires. They don't type the generic parameter, losing IDE autocomplete. They misunderstand that the Observable is lazy.
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(private http: HttpClient) {}
createUser(user: User): Observable<User> {
return this.http.post<User>('/api/users', user);
}
updateUser(id: number, data: Partial<User>): Observable<User> {
return this.http.put<User>('/api/users/' + id, data);
}
deleteUser(id: number): Observable<void> {
return this.http.delete<void>('/api/users/' + id);
}
}
// Component
this.userService.createUser({ name: 'John', email: 'john@test.com' })
.subscribe({
next: (created) => console.log('Created:', created),
error: (err) => console.error('Error:', err)
});
HttpClient automatically serializes objects to JSON and sets the Content-Type header. Other methods: put(), patch(), delete().
Why it matters: Tests knowledge of HTTP methods and REST conventions. Interviewers verify you know when to use POST vs PUT vs PATCH and understand the differences.
Real applications: Creating users/posts via POST, updating entire records via PUT, partial updates via PATCH, and deleting via DELETE. Each method is essential for RESTful APIs.
Common mistakes: Developers use POST for everything instead of understanding HTTP semantics. They confuse PUT (replace entire resource) with PATCH (partial update). They forget to handle errors on mutating operations.
@Injectable({ providedIn: 'root' })
export class DataService {
constructor(private http: HttpClient) {}
getData(): Observable<Data[]> {
return this.http.get<Data[]>('/api/data').pipe(
catchError(this.handleError)
);
}
private handleError(error: HttpErrorResponse): Observable<never> {
let message = 'An error occurred';
if (error.status === 0) {
message = 'Network error. Check your connection.';
} else if (error.status === 404) {
message = 'Resource not found.';
} else if (error.status === 500) {
message = 'Server error. Try again later.';
}
console.error(message, error.message);
return throwError(() => new Error(message));
}
}
HttpErrorResponse contains status, statusText, error (body), and message. A status of 0 typically indicates a network error or CORS issue.
Why it matters: Tests error handling patterns and understanding of Observable error handling with RxJS. Shows you can build resilient applications that handle failures gracefully.
Real applications: Displaying user-friendly error messages, logging errors for monitoring, retrying failed requests, and handling CORS issues. Production apps must handle network failures elegantly.
Common mistakes: Developers ignore errors entirely, leaving users with silent failures. They catch errors but don't log them for debugging. They don't handle CORS errors differently from data errors.
import { HttpHeaders } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class ApiService {
constructor(private http: HttpClient) {}
getData(token: string): Observable<any> {
const headers = new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
});
return this.http.get('/api/data', { headers });
}
// Headers are immutable; set() returns a new instance
postData(data: any): Observable<any> {
let headers = new HttpHeaders();
headers = headers.set('X-Custom-Header', 'value');
headers = headers.append('Accept', 'application/json');
return this.http.post('/api/data', data, { headers });
}
}
HttpHeaders is immutable: set(), append(), and delete() return new instances. For auth headers, prefer using an HTTP interceptor to avoid repetition.
Why it matters: Tests understanding of immutability patterns and practical HTTP header usage. Shows you follow DRY principles by centralizing common headers.
Real applications: Adding authentication tokens, setting custom headers for APIs, CORS headers, and content-type declarations. Production APIs often require specific headers for routing and security.
Common mistakes: Developers forget that HttpHeaders is immutable, trying to mutate in place. They repeat header logic across multiple requests instead of using interceptors. They don't understand why immutability matters.
import { HttpParams } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class SearchService {
constructor(private http: HttpClient) {}
search(term: string, page: number, limit: number): Observable<Results> {
const params = new HttpParams()
.set('q', term)
.set('page', page.toString())
.set('limit', limit.toString());
return this.http.get<Results>('/api/search', { params });
// URL: /api/search?q=angular&page=1&limit=10
}
// Alternative: pass object directly (Angular 15+)
searchSimple(term: string): Observable<Results> {
return this.http.get<Results>('/api/search', {
params: { q: term, page: '1' }
});
}
}
Like HttpHeaders, HttpParams is immutable. Each set() or append() call returns a new instance. Use append() to add multiple values for the same key.
Why it matters: Tests understanding of URL query parameters and immutability. Shows you can build type-safe query strings without manual string concatenation.
Real applications: Pagination (page, limit), filtering (category, status), searching (q), and sorting (sort_by). Query parameters are essential for any API communication with optional data.
Common mistakes: Developers manually concatenate URLs like '/search?q=' + term, leading to encoding issues. They don't realize HttpParams handles URL encoding automatically. They forget HttpParams is immutable like HttpHeaders.
interface User {
id: number;
name: string;
email: string;
}
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(private http: HttpClient) {}
// Typed response: Observable<User[]>
getUsers(): Observable<User[]> {
return this.http.get<User[]>('/api/users');
}
// Full response with headers and status
getUsersFull(): Observable<HttpResponse<User[]>> {
return this.http.get<User[]>('/api/users', { observe: 'response' });
}
// Access headers from full response
checkHeaders() {
this.getUsersFull().subscribe(resp => {
console.log('Status:', resp.status);
console.log('Total:', resp.headers.get('X-Total-Count'));
console.log('Body:', resp.body);
});
}
}
Use observe: 'response' to get the full HttpResponse with headers, status, and body. Use observe: 'events' for progress tracking.
Why it matters: Tests knowledge of advanced HttpClient features and when to use the full response object. Shows understanding that APIs return more than just data.
Real applications: Reading pagination headers (X-Total-Count), checking rate limit headers, handling CORS headers, and extracting custom server headers for business logic.
Common mistakes: Developers don't realize response headers exist because they default to observe: 'body'. They lose important pagination or rate-limit information. They hardcode expected headers instead of reading them dynamically.
import { retry, timer, retryWhen, mergeMap, throwError } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class ApiService {
constructor(private http: HttpClient) {}
// Simple retry (3 attempts)
getData(): Observable<Data> {
return this.http.get<Data>('/api/data').pipe(
retry(3),
catchError(err => throwError(() => err))
);
}
// Retry with delay
getDataWithDelay(): Observable<Data> {
return this.http.get<Data>('/api/data').pipe(
retry({ count: 3, delay: 1000 }),
catchError(err => throwError(() => err))
);
}
}
The retry operator resubscribes to the source Observable on error. Use delay to add wait time between retries. Only retry on transient errors (5xx, network issues) — not on client errors (4xx).
Why it matters: Tests understanding of resilient application design and RxJS operators. Shows you can handle transient failures gracefully without user intervention.
Real applications: Network timeouts that resolve after retry, temporary server issues, and rate limiting scenarios all benefit from automatic retry logic with exponential backoff.
Common mistakes: Developers retry on all errors including 400/404, wasting time on permanent failures. They don't use delays between retries, hammering the server harder. They retry forever without a limit.
import { HttpEventType } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class UploadService {
constructor(private http: HttpClient) {}
uploadFile(file: File): Observable<number> {
const formData = new FormData();
formData.append('file', file);
return this.http.post('/api/upload', formData, {
reportProgress: true,
observe: 'events'
}).pipe(
map(event => {
switch (event.type) {
case HttpEventType.UploadProgress:
return Math.round(100 * (event.loaded / (event.total || 1)));
case HttpEventType.Response:
return 100;
default:
return 0;
}
})
);
}
}
Event types include: Sent, UploadProgress, ResponseHeader, DownloadProgress, and Response. This is commonly used for file upload progress bars.
Why it matters: Tests knowledge of advanced HttpClient features and Observable event handling. Shows you can provide user feedback on long-running operations.
Real applications: File uploads displaying progress bars, large file downloads with percentage completion, and streaming data where users need feedback on operation status.
Common mistakes: Developers try to track progress without setting reportProgress: true and observe: 'events'. They don't filter events by type, causing errors. They send HTTP events directly to the UI instead of calculating percentages.
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = this.authService.getToken();
if (token) {
const cloned = req.clone({
setHeaders: { Authorization: 'Bearer ' + token }
});
return next.handle(cloned);
}
return next.handle(req);
}
}
// Register in module
@NgModule({
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
]
})
Interceptors are registered with the HTTP_INTERCEPTORS multi-provider token. Requests pass through interceptors in registration order. The request is immutable; use clone() to modify it.
Why it matters: Tests deep understanding of cross-cutting concerns in HTTP and Angular architecture. Interceptors are a key pattern for enterprise applications.
Real applications: Authentication (adding tokens), error handling (global error responses), request/response logging, CORS handling, and request rate limiting all centralized in interceptors.
Common mistakes: Developers forget the multi-provider pattern, overwriting previous interceptors. They mutate the original request instead of cloning. They don't understand interceptor order matters for request preprocessing.
// Default: returns body only
this.http.get<User[]>('/api/users')
.subscribe(users => console.log(users));
// Full response: includes status, headers, body
this.http.get<User[]>('/api/users', { observe: 'response' })
.subscribe(resp => {
console.log(resp.status);
console.log(resp.headers.get('X-Total-Count'));
console.log(resp.body);
});
Use observe: 'body' for simple data fetching. Use 'response' when you need headers or status codes for pagination, caching, or error differentiation.
Why it matters: Tests understanding of HttpClient options and when to use advanced features. Shows you know how to access metadata from HTTP responses.
Real applications: Implementing pagination by reading X-Total-Count header, checking rate limits from response headers, implementing caching based on response metadata.
Common mistakes: Developers default to observe: 'body' everywhere, missing valuable header information. They don't realize headers contain business-critical data like pagination totals.
// map: transform the response data
this.http.get<ApiResponse>('/api/data').pipe(
map(response => response.results) // transform value
);
// switchMap: chain requests, cancel previous
this.searchInput.valueChanges.pipe(
debounceTime(300),
switchMap(term => this.http.get('/api/search?q=' + term))
// new search cancels the previous HTTP request
);
switchMap is ideal for search-as-you-type because it cancels outdated requests. Use concatMap if you need to preserve all requests in order, or mergeMap for parallel execution.
Why it matters: Tests understanding of RxJS higher-order operators and their practical differences. Shows you can optimize reactive streams for specific scenarios.
Real applications: Search-as-you-type needs switchMap to cancel old searches. Sequential operations like migrations need concatMap. Independent parallel requests use mergeMap.
Common mistakes: Developers use map() instead of switchMap/mergeMap, getting nested Observables. They don't understand switchMap cancels previous requests, leaving dangling subscriptions. They use the wrong operator for the use case.
// Method 1: unsubscribe directly
const sub = this.http.get('/api/data').subscribe(data => {});
sub.unsubscribe(); // cancels the HTTP request
// Method 2: takeUntil pattern
private destroy$ = new Subject<void>();
this.http.get('/api/data').pipe(
takeUntil(this.destroy$)
).subscribe();
ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); }
// Method 3: switchMap auto-cancels previous
searchTerm$.pipe(
switchMap(term => this.http.get('/api/search?q=' + term))
);
The takeUntil pattern is the most common approach for cleaning up subscriptions in components. The async pipe handles unsubscription automatically in templates.
Why it matters: Tests understanding of subscription management and memory leak prevention. Shows you know how to avoid memory leaks in Angular components.
Real applications: Canceling in-flight searches when user navigates away, stopping uploads when component destroys, preventing API calls to stale components.
Common mistakes: Developers never unsubscribe, creating memory leaks that accumulate over time. They don't understand that components destroyed with active subscriptions cause errors. They forget the destroy$ pattern pattern entirely.
// In app.config.ts for standalone apps
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([authInterceptor, loggingInterceptor]),
withFetch() // use Fetch API
)
]
};
// Bootstrap
bootstrapApplication(AppComponent, appConfig);
Use withInterceptorsFromDi() if you need to support class-based interceptors alongside functional ones. The withFetch() option switches from XMLHttpRequest to the modern Fetch API.
Why it matters: Tests knowledge of modern Angular (15+) and standalone APIs. Shows you understand the evolution from module-based to standalone architecture.
Real applications: All new Angular 15+ applications use provideHttpClient with standalone components. Feature functions allow selective HTTP interception and API configuration.
Common mistakes: Developers mix module-based HttpClientModule with standalone apps. They don't know about provideHttpClient or feature functions. They import HttpClientModule in standalone apps creating incompatibility.
import { forkJoin } from 'rxjs';
// Make parallel requests
forkJoin({
users: this.http.get<User[]>('/api/users'),
posts: this.http.get<Post[]>('/api/posts'),
settings: this.http.get<Settings>('/api/settings')
}).subscribe(({ users, posts, settings }) => {
this.users = users;
this.posts = posts;
this.settings = settings;
});
forkJoin waits for all requests to complete before emitting. Use combineLatest if you want to react to partial results as they arrive.
Why it matters: Tests understanding of advanced RxJS patterns for coordinating multiple Observables. Shows you can handle complex async scenarios efficiently.
Real applications: Dashboard pages loading users, posts, comments, and settings in parallel. Initialization sequences needing all data before rendering can use forkJoin.
Common mistakes: Developers make sequential HTTP calls when they should parallel with forkJoin. They don't handle the case where one request fails, failing the entire forkJoin. They use combineLatest when they mean forkJoin.