From frontend-angular
Angular data fetching patterns including HttpClient, interceptors, resolvers, RxJS operators, caching, loading/error state management, and SSR TransferState.
How this skill is triggered — by the user, by Claude, or both
Slash command
/frontend-angular:angular-data-fetchingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Inject `HttpClient` using the `inject()` function. Always use typed responses.
Inject HttpClient using the inject() function. Always use typed responses.
// app.config.ts
import { provideHttpClient, withInterceptors, withFetch } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withFetch(), // use the Fetch API (recommended)
withInterceptors([authInterceptor, errorInterceptor]),
),
],
};
@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);
private baseUrl = '/api/users';
getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.baseUrl);
}
getUser(id: string): Observable<User> {
return this.http.get<User>(`${this.baseUrl}/${id}`);
}
createUser(data: CreateUserDto): Observable<User> {
return this.http.post<User>(this.baseUrl, data);
}
updateUser(id: string, data: Partial<User>): Observable<User> {
return this.http.patch<User>(`${this.baseUrl}/${id}`, data);
}
deleteUser(id: string): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}/${id}`);
}
}
// Access full response (status, headers, body)
this.http.get<User>('/api/user/1', { observe: 'response' }).subscribe(response => {
console.log(response.status); // 200
console.log(response.headers);
console.log(response.body); // User | null
});
// Get raw text
this.http.get('/api/export', { responseType: 'text' });
// Get blob (file download)
this.http.get('/api/file', { responseType: 'blob' });
// With query params
this.http.get<User[]>('/api/users', {
params: new HttpParams().set('page', 1).set('size', 20),
});
Use functional interceptors (Angular 15+) instead of class-based interceptors.
// auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService);
const token = auth.getToken();
if (token) {
const authReq = req.clone({
setHeaders: { Authorization: `Bearer ${token}` },
});
return next(authReq);
}
return next(req);
};
// error.interceptor.ts
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, throwError } from 'rxjs';
import { Router } from '@angular/router';
import { NotificationService } from './notification.service';
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const router = inject(Router);
const notify = inject(NotificationService);
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
switch (error.status) {
case 401:
router.navigate(['/login']);
break;
case 403:
notify.error('You do not have permission to perform this action.');
break;
case 500:
notify.error('A server error occurred. Please try again later.');
break;
}
return throwError(() => error);
})
);
};
// retry.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { retry, timer } from 'rxjs';
export const retryInterceptor: HttpInterceptorFn = (req, next) => {
// Only retry GET requests
if (req.method !== 'GET') return next(req);
return next(req).pipe(
retry({
count: 3,
delay: (error, retryCount) => timer(retryCount * 1000), // 1s, 2s, 3s
})
);
};
Use switchMap when a new value should cancel the previous request (e.g., search, route param changes).
@Component({ standalone: true, ... })
export class UserDetailComponent {
private route = inject(ActivatedRoute);
private userService = inject(UserService);
user = toSignal(
this.route.paramMap.pipe(
map(params => params.get('id')!),
switchMap(id => this.userService.getUser(id))
)
);
}
getUsers(): Observable<User[]> {
return this.http.get<User[]>('/api/users').pipe(
catchError((error: HttpErrorResponse) => {
console.error('Failed to load users', error);
return of([]); // return empty array as fallback
})
);
}
Avoid duplicate HTTP calls when multiple subscribers consume the same observable.
@Injectable({ providedIn: 'root' })
export class ConfigService {
private config$ = this.http.get<AppConfig>('/api/config').pipe(
shareReplay(1) // cache the last emission; replay to late subscribers
);
getConfig(): Observable<AppConfig> {
return this.config$;
}
}
const data$ = combineLatest({
user: this.userService.getUser(id),
posts: this.postService.getUserPosts(id),
followers: this.followService.getFollowers(id),
}).pipe(
map(({ user, posts, followers }) => ({ user, posts, followers }))
);
// All observables complete before emitting
const result$ = forkJoin({
products: this.productService.getAll(),
categories: this.categoryService.getAll(),
});
Manage loading and error state explicitly.
@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@if (loading()) {
<app-spinner />
} @else if (error()) {
<app-error-message [message]="error()!" />
} @else {
@for (user of users(); track user.id) {
<app-user-card [user]="user" />
}
}
`,
})
export class UserListComponent implements OnInit {
private userService = inject(UserService);
users = signal<User[]>([]);
loading = signal(false);
error = signal<string | null>(null);
ngOnInit() {
this.loading.set(true);
this.userService.getUsers().subscribe({
next: users => {
this.users.set(users);
this.loading.set(false);
},
error: err => {
this.error.set(err.message);
this.loading.set(false);
},
});
}
}
@Component({ standalone: true, ... })
export class ProductListComponent {
private productService = inject(ProductService);
private request$ = this.productService.getProducts().pipe(
map(products => ({ products, error: null, loading: false })),
startWith({ products: [], error: null, loading: true }),
catchError(err => of({ products: [], error: err.message, loading: false }))
);
vm = toSignal(this.request$, { initialValue: { products: [], error: null, loading: true } });
}
@Injectable({ providedIn: 'root' })
export class UserCacheService {
private cache = new Map<string, Observable<User>>();
getUser(id: string): Observable<User> {
if (!this.cache.has(id)) {
const req$ = this.http.get<User>(`/api/users/${id}`).pipe(
shareReplay(1),
tap({ error: () => this.cache.delete(id) }) // evict on error
);
this.cache.set(id, req$);
}
return this.cache.get(id)!;
}
invalidate(id: string) {
this.cache.delete(id);
}
}
export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
if (req.method !== 'GET') return next(req);
return next(req.clone({
setHeaders: { 'Cache-Control': 'max-age=300' },
}));
};
Use functional resolvers to ensure data is available before a component renders.
// product.resolver.ts
export const productResolver: ResolveFn<Product> = (route) => {
const productService = inject(ProductService);
const router = inject(Router);
const id = route.paramMap.get('id')!;
return productService.getProduct(id).pipe(
catchError(() => {
router.navigate(['/not-found']);
return EMPTY;
})
);
};
// Route config
{
path: 'products/:id',
component: ProductDetailComponent,
resolve: { product: productResolver },
}
// Component reads resolved data (no loading state needed)
@Component({ standalone: true, ... })
export class ProductDetailComponent {
product = input<Product>(); // bound via withComponentInputBinding()
}
When using Angular Universal (SSR), avoid duplicate HTTP requests between server and browser by transferring state.
// app.config.server.ts
import { provideHttpClient, withFetch } from '@angular/common/http';
export const serverConfig: ApplicationConfig = {
providers: [
provideHttpClient(withFetch()),
],
};
Use provideClientHydration(withHttpTransferCache()) to automatically cache HTTP responses from SSR and replay them in the browser:
// app.config.ts
import { provideClientHydration, withHttpTransferCache } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(withHttpTransferCache()),
provideHttpClient(withFetch()),
],
};
This eliminates duplicate server-to-browser requests without manual TransferState management.
@Injectable({ providedIn: 'root' })
export class ProductService {
private http = inject(HttpClient);
private transferState = inject(TransferState);
private platform = inject(PLATFORM_ID);
private PRODUCTS_KEY = makeStateKey<Product[]>('products');
getProducts(): Observable<Product[]> {
if (isPlatformBrowser(this.platform)) {
const cached = this.transferState.get(this.PRODUCTS_KEY, null);
if (cached) {
this.transferState.remove(this.PRODUCTS_KEY);
return of(cached);
}
}
return this.http.get<Product[]>('/api/products').pipe(
tap(products => {
if (isPlatformServer(this.platform)) {
this.transferState.set(this.PRODUCTS_KEY, products);
}
})
);
}
}
Prefer withHttpTransferCache() over manual TransferState for HTTP responses.
npx claudepluginhub gagandeepp/software-agent-teams --plugin frontend-angularGuides completion of development work by verifying tests, detecting environment, and presenting structured options for merge, PR, or cleanup.
Enforces test-driven development: write failing test first, then minimal code to pass. Use when implementing features or bugfixes.
Guides creation and editing of skills using test-driven development with pressure scenarios and subagents to verify agent compliance.