skills/api-client-angular/SKILL.md
Build typed API client services for Angular 19 frontends. Covers HttpClient configuration, typed interceptors, error handling, request/response types from API envelope, auth header injection, and retry patterns. Use when: creating Angular API services, handling API errors, building typed HttpClient wrappers, or integrating with the backend ApiResponse envelope.
npx skillsauth add congiuluc/my-awesome-copilot api-client-angularInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
ApiResponse<T> envelopeHttpClient configuration — set up base URL and interceptors in provideHttpClient().ApiResponse<T> envelope to typed results.response.success, throw typed errors.pipe(), map(), catchError() for clean data flows.// app.config.ts
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])
),
],
};
// models/api-response.model.ts
export interface ApiResponse<T> {
success: boolean;
data: T | null;
error: string | null;
}
// interceptors/auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from '../services/auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
const token = authService.getAccessToken();
if (token) {
req = req.clone({
setHeaders: { Authorization: `Bearer ${token}` },
});
}
return next(req);
};
// interceptors/error.interceptor.ts
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { catchError, throwError } from 'rxjs';
export class ApiError extends Error {
constructor(
message: string,
public status: number,
public code?: string
) {
super(message);
this.name = 'ApiError';
}
}
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
const apiError = new ApiError(
error.error?.error ?? error.statusText,
error.status
);
return throwError(() => apiError);
})
);
};
// services/api.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, map } from 'rxjs';
import { environment } from '../../environments/environment';
import { ApiResponse } from '../models/api-response.model';
@Injectable({ providedIn: 'root' })
export class ApiService {
private readonly http = inject(HttpClient);
private readonly baseUrl = environment.apiUrl;
get<T>(path: string): Observable<T> {
return this.http.get<ApiResponse<T>>(`${this.baseUrl}${path}`).pipe(
map((envelope) => this.unwrap(envelope))
);
}
post<T>(path: string, body: unknown): Observable<T> {
return this.http.post<ApiResponse<T>>(`${this.baseUrl}${path}`, body).pipe(
map((envelope) => this.unwrap(envelope))
);
}
put<T>(path: string, body: unknown): Observable<T> {
return this.http.put<ApiResponse<T>>(`${this.baseUrl}${path}`, body).pipe(
map((envelope) => this.unwrap(envelope))
);
}
delete<T>(path: string): Observable<T> {
return this.http.delete<ApiResponse<T>>(`${this.baseUrl}${path}`).pipe(
map((envelope) => this.unwrap(envelope))
);
}
private unwrap<T>(envelope: ApiResponse<T>): T {
if (!envelope.success) {
throw new ApiError(envelope.error ?? 'Unknown error', 0);
}
return envelope.data as T;
}
}
// services/product.service.ts
import { Injectable, inject } from '@angular/core';
import { ApiService } from './api.service';
import { Product, CreateProductRequest } from '../models/product.model';
@Injectable({ providedIn: 'root' })
export class ProductService {
private readonly api = inject(ApiService);
getAll() {
return this.api.get<Product[]>('/products');
}
getById(id: string) {
return this.api.get<Product>(`/products/${encodeURIComponent(id)}`);
}
create(request: CreateProductRequest) {
return this.api.post<Product>('/products', request);
}
update(id: string, request: Partial<CreateProductRequest>) {
return this.api.put<Product>(`/products/${encodeURIComponent(id)}`, request);
}
delete(id: string) {
return this.api.delete<void>(`/products/${encodeURIComponent(id)}`);
}
}
Observable, let consumers subscribeAuthorization headers per request — use the auth interceptorApiResponse<T> in the base serviceany for response types — always type the responseencodeURIComponent for path paramstools
Build VS Code extensions with TypeScript. Covers extension anatomy, activation events, commands, tree views, webview panels, language features, testing, and publishing. Use when: creating a new VS Code extension, adding commands/views/providers, building webview UIs, implementing language server features, testing extensions, or packaging for the marketplace.
development
Track implementations, features, bugs, and releases in a versioning document. Use when: adding a commit, completing a feature, fixing a bug, or preparing a release. Automatically updates CHANGELOG.md following Keep a Changelog format and Semantic Versioning.
development
Write frontend tests using Vitest and React Testing Library. Use when: testing React components, hooks, user interactions, form submissions, accessibility assertions, or mocking API services.
development
Write Angular frontend tests using Jasmine, Karma, and Angular TestBed. Use when: testing Angular components, services, pipes, directives, user interactions, form submissions, accessibility assertions, or mocking HTTP services.