@Injectable()
export class AuthInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const authReq = req.clone({
setHeaders: { Authorization: 'Bearer ' + this.token }
});
return next.handle(authReq);
}
}
Requests are immutable, so you must use clone() to create a modified copy. The next.handle() passes the request to the next interceptor or the backend.
Why it matters: Tests understanding of cross-cutting concerns and middleware patterns in Angular. Shows knowledge of how to implement enterprise-level features globally.
Real applications: Authentication, logging, error centralization, and caching all implemented via interceptors rather than repeating code in every service.
Common mistakes: Developers forget to clone the immutable request, causing errors. They don't understand interceptors sit between HttpClient and the server. They use interceptors for things better handled at the component level.
// Module-based
providers: [{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}]
// Standalone (Angular 15+)
provideHttpClient(withInterceptors([authInterceptor]))
The multi: true flag is required to allow multiple interceptors. Without it, each registration would overwrite the previous one.
Why it matters: Tests understanding of Angular dependency injection and multi-provider tokens. Shows you can register multiple instances of a provider.
Real applications: Every production app with HTTP needs at least one interceptor. Most apps have auth, logging, and error-handling interceptors all registered together.
Common mistakes: Developers forget the multi: true flag, accidentally overwriting previous interceptors. They don't understand the difference between module-based and standalone registration patterns.
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
console.log('Request:', req.url);
return next(req).pipe(
tap(event => {
if (event instanceof HttpResponse) {
console.log('Response:', event.status);
}
})
);
};
Functional interceptors are the recommended approach for new Angular projects. They avoid the boilerplate of creating injectable classes.
Why it matters: Tests knowledge of modern Angular (15+) evolution from class-based to functional patterns. Shows you're familiar with contemporary Angular best practices.
Real applications: All new standalone Angular applications use functional interceptors. They're simpler and more concise than class-based alternatives.
Common mistakes: Developers still use class-based interceptors in new projects. They don't know about the inject() API for functional dependencies. They confuse functional interceptors with middleware.
intercept(req: HttpRequest<any>, next: HttpHandler) {
return next.handle(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
this.auth.logout();
this.router.navigate(['/login']);
}
return throwError(() => error);
})
);
}
Always re-throw the error with throwError so that individual service calls can still handle specific errors. This provides both global and local error handling.
Why it matters: Tests understanding of error handling architecture and RxJS error operators. Shows you can centralize error handling while allowing specific overrides.
Real applications: 401 redirects to login, 403 shows forbidden message, 5xx shows retry dialogs all centralized in one interceptor.
Common mistakes: Developers swallow errors entirely, preventing services from handling them. They don't re-throw, breaking downstream error handling. They try to show user messages in the interceptor instead of passing them through.
intercept(req: HttpRequest<any>, next: HttpHandler) {
return next.handle(req).pipe(
retry({ count: 3, delay: 1000 }),
catchError(err => throwError(() => err))
);
}
Only retry idempotent requests like GET. Retrying POST requests can cause duplicate data. Add conditions to skip retry for non-GET methods.
Why it matters: Tests understanding of idempotency and resilient systems. Shows you know the risks of blindly retrying all requests.
Real applications: Network timeouts on data fetching auto-retry. File uploads with transient failures retry automatically. Race conditions avoided by only retrying safe operations.
Common mistakes: Developers retry POST/DELETE requests, creating duplicate orders or data. They don't add delays between retries. They retry forever without a limit.
return next.handle(req).pipe(
map(event => {
if (event instanceof HttpResponse) {
return event.clone({ body: event.body.data });
}
return event;
})
);
Check for HttpResponse instances since the Observable also emits other event types. Only modify the final response, not intermediate events like progress updates.
Why it matters: Tests understanding of Observable event types and response transformation patterns. Shows you can normalize API responses transparently.
Real applications: APIs often wrap responses in envelopes like { data: {...}, meta: {...} }. Interceptors unwrap these transparently, simplifying component code.
Common mistakes: Developers modify all events, including progress updates. They don't clone the response, mutating the original. They don't check for HttpResponse type, causing errors on other events.
// Registration order
provideHttpClient(withInterceptors([
loggingInterceptor, // 1st request, 3rd response
authInterceptor, // 2nd request, 2nd response
cacheInterceptor // 3rd request, 1st response
]))
Place logging interceptors first to capture all requests. Place cache interceptors last so cached responses skip other interceptors.
Why it matters: Tests understanding of interceptor composition and execution order. Shows you know how to architect interceptor chains for efficiency.
Real applications: Logging first to capture all requests, auth second to add tokens, cache last to avoid modifying cached data.
Common mistakes: Developers don't consider ordering, causing unexpected behavior. They place the cache interceptor first, logging transformations instead of raw requests. They don't understand reverse order for responses.
// Set a marker header
const req = new HttpRequest('GET', url, { headers: new HttpHeaders({ 'Skip-Auth': 'true' }) });
// In interceptor
if (req.headers.has('Skip-Auth')) {
return next.handle(req.clone({ headers: req.headers.delete('Skip-Auth') }));
}
The HttpContext API is preferred over custom headers as it does not leak internal markers to the server. Context tokens are type-safe and invisible to the backend.
Why it matters: Tests knowledge of advanced HttpClient APIs and clean architecture patterns. Shows you can skip specific interceptor logic selectively.
Real applications: Bypassing auth interceptor for public APIs, skipping cache for fresh data requests, excluding specific endpoints from logging.
Common mistakes: Developers use custom headers that leak to the server. They don't know about HttpContext. They hardcode skip logic instead of making it configurable.
intercept(req: HttpRequest<any>, next: HttpHandler) {
this.loadingService.show();
return next.handle(req).pipe(
finalize(() => this.loadingService.hide())
);
}
Use a counter in the LoadingService to handle multiple concurrent requests. Only hide the spinner when the counter reaches zero.
Why it matters: Tests understanding of concurrent HTTP request handling and user feedback mechanisms. Shows you can provide visual feedback for all API activity.
Real applications: Any app with async data needs a loading indicator. This interceptor makes every HTTP request automatically show loading UI without component code.
Common mistakes: Developers hide the spinner on first request completion, ignoring other concurrent requests. They don't use a counter. They hardcode the spinner in components instead of centralizing.
intercept(req: HttpRequest<any>, next: HttpHandler) {
// Modify request
const modifiedReq = req.clone({ setHeaders: { 'X-Custom': 'value' } });
// Handle response
return next.handle(modifiedReq).pipe(
tap(event => {
if (event instanceof HttpResponse) {
console.log('Response received:', event.status);
}
})
);
}
Use tap for side effects like logging and map for transforming the response body. Keep request and response logic in the same interceptor when they are related.
Why it matters: Tests understanding of RxJS operators and symmetrical request/response handling. Shows you know how to implement bidirectional interceptor logic.
Real applications: Add auth token on request, log response status. Add request headers, transform response format.
Common mistakes: Developers create separate interceptors for requests and responses when they should be together. They use map for logging instead of tap. They don't understand that interceptors wrap both directions.
// Functional interceptor (modern)
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).getToken();
const authReq = req.clone({ setHeaders: { Authorization: 'Bearer ' + token } });
return next(authReq);
};
// Register
provideHttpClient(withInterceptors([authInterceptor]))
New projects should prefer functional interceptors as they are the modern approach. They avoid the boilerplate of creating injectable classes.
Why it matters: Tests knowledge of Angular evolution and when to use modern patterns. Functional interceptors are the recommended standard going forward.
Real applications: New standalone components use functional interceptors with provideHttpClient(). Legacy module-based apps still use class-based interceptors.
Common mistakes: Developers continue using class-based patterns in new projects. They don't realize functional interceptors are simpler and more testable. They mix both patterns confusingly.
export const refreshInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService);
return next(req).pipe(
catchError(error => {
if (error.status === 401) {
return auth.refreshToken().pipe(
switchMap(newToken => {
const retryReq = req.clone({
setHeaders: { Authorization: 'Bearer ' + newToken }
});
return next(retryReq);
})
);
}
return throwError(() => error);
})
);
};
Use switchMap to chain the retry after the token refresh. Handle cases where the refresh token itself is expired by redirecting to login.
Why it matters: Tests deep understanding of authentication patterns and RxJS operators in real-world scenarios. Shows you can implement sophisticated security flows.
Real applications: SPA apps with JWT tokens need token refresh logic. When tokens expire (401), fetch new token and retry original request seamlessly.
Common mistakes: Developers don't handle multiple concurrent 401 responses, causing multiple refresh attempts. They don't queue requests during refresh. They don't handle refresh token expiration.
const cache = new Map<string, HttpResponse<any>>();
export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
if (req.method !== 'GET') return next(req);
const cached = cache.get(req.urlWithParams);
if (cached) return of(cached.clone());
return next(req).pipe(
tap(event => {
if (event instanceof HttpResponse) {
cache.set(req.urlWithParams, event.clone());
}
})
);
};
This reduces server load and improves responsiveness for data that does not change frequently. Add cache invalidation logic for mutable data.
Why it matters: Tests understanding of performance optimization and caching strategies. Shows you know how to reduce unnecessary API calls.
Real applications: Caching product lists, user profiles, and config data. Skipping cache for refresh requests or time-limited cache expiration.
Common mistakes: Developers cache all requests indiscriminately, showing stale data. They don't implement cache invalidation leading to outdated information. They cache mutable operations like POST/PUT.
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
const startTime = Date.now();
console.log('Request:', req.method, req.url);
return next(req).pipe(
tap(event => {
if (event instanceof HttpResponse) {
const duration = Date.now() - startTime;
console.log('Response:', req.url, event.status, duration + 'ms');
}
})
);
};
In production, send logs to a monitoring service instead of the console. Use the finalize operator to also capture failed requests.
Why it matters: Tests understanding of observability and debugging in production. Shows you know how to track API performance and errors.
Real applications: Every production app needs request logging for performance monitoring, error tracking, and debugging user issues.
Common mistakes: Developers log to console in production, overwhelming logs. They don't log failures. They don't measure timing or include request/response sizes.
export const unwrapInterceptor: HttpInterceptorFn = (req, next) => {
return next(req).pipe(
map(event => {
if (event instanceof HttpResponse && event.body?.data) {
// Unwrap: { data: [...], meta: {...} } → [...]
return event.clone({ body: event.body.data });
}
return event;
})
);
};
Common transformations include unwrapping envelopes, converting date strings to Date objects, and adding default values to responses.
Why it matters: Tests understanding of response transformation and API normalization. Shows you can adapt component code to any API response format.
Real applications: APIs often return wrapped responses. Interceptors can normalize to match your component types, simplifying business logic.
Common mistakes: Developers unwrap responses in components repeatedly instead of centralizing in interceptors. They don't verify the response has the expected structure. They transform all events instead of just HttpResponse.