Claude-skill-registry angular-api-service
Use when creating API services for backend communication with proper patterns for caching, error handling, and type safety.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/frontend-angular-api-service" ~/.claude/skills/majiayu000-claude-skill-registry-angular-api-service && rm -rf "$T"
manifest:
skills/data/frontend-angular-api-service/SKILL.mdsource content
Angular API Service Development Workflow
When to Use This Skill
- Creating new API service for backend communication
- Adding caching to API calls
- Implementing file upload/download
- Adding custom headers or interceptors
Pre-Flight Checklist
- Identify backend API base URL
- Read the design system docs for the target application (see below)
- List all endpoints to implement
- Determine caching requirements
- Search existing services:
grep "{Feature}ApiService" --include="*.ts"
🎨 Design System Documentation (MANDATORY)
Before creating any API service, read the design system documentation for your target application:
| Application | Design System Location |
|---|---|
| WebV2 Apps | |
| TextSnippetClient | |
Key docs to read:
- Component overview, base classes, library summaryREADME.md
- Implementation checklist, best practices07-technical-guide.md
- State management and API integration patterns06-state-management.md
File Location
src/PlatformExampleAppWeb/libs/apps-domains/src/lib/ └── {domain}/ └── services/ └── {feature}-api.service.ts
Pattern 1: Basic CRUD API Service
// {feature}-api.service.ts import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { PlatformApiService } from '@libs/platform-core'; import { environment } from '@env/environment'; // ═══════════════════════════════════════════════════════════════════════════ // DTOs (can be in separate file) // ═══════════════════════════════════════════════════════════════════════════ export interface FeatureDto { id: string; name: string; code: string; status: FeatureStatus; createdDate: Date; } export interface FeatureListQuery { searchText?: string; statuses?: FeatureStatus[]; skipCount?: number; maxResultCount?: number; } export interface PagedResult<T> { items: T[]; totalCount: number; } export interface SaveFeatureCommand { id?: string; name: string; code: string; status: FeatureStatus; } // ═══════════════════════════════════════════════════════════════════════════ // API SERVICE // ═══════════════════════════════════════════════════════════════════════════ @Injectable({ providedIn: 'root' }) export class FeatureApiService extends PlatformApiService { // ───────────────────────────────────────────────────────────────────────── // CONFIGURATION // ───────────────────────────────────────────────────────────────────────── protected get apiUrl(): string { return environment.apiUrl + '/api/Feature'; } // ───────────────────────────────────────────────────────────────────────── // QUERY METHODS // ───────────────────────────────────────────────────────────────────────── getList(query?: FeatureListQuery): Observable<PagedResult<FeatureDto>> { return this.get<PagedResult<FeatureDto>>('', query); } getById(id: string): Observable<FeatureDto> { return this.get<FeatureDto>(`/${id}`); } getByCode(code: string): Observable<FeatureDto> { return this.get<FeatureDto>('/by-code', { code }); } // ───────────────────────────────────────────────────────────────────────── // COMMAND METHODS // ───────────────────────────────────────────────────────────────────────── save(command: SaveFeatureCommand): Observable<FeatureDto> { return this.post<FeatureDto>('', command); } update(id: string, command: Partial<SaveFeatureCommand>): Observable<FeatureDto> { return this.put<FeatureDto>(`/${id}`, command); } delete(id: string): Observable<void> { return this.deleteRequest<void>(`/${id}`); } // ───────────────────────────────────────────────────────────────────────── // VALIDATION METHODS // ───────────────────────────────────────────────────────────────────────── checkCodeExists(code: string, excludeId?: string): Observable<boolean> { return this.get<boolean>('/check-code-exists', { code, excludeId }); } }
Pattern 2: API Service with Caching
@Injectable({ providedIn: 'root' }) export class LookupApiService extends PlatformApiService { protected get apiUrl(): string { return environment.apiUrl + '/api/Lookup'; } // ───────────────────────────────────────────────────────────────────────── // CACHED METHODS // ───────────────────────────────────────────────────────────────────────── getCountries(): Observable<CountryDto[]> { return this.get<CountryDto[]>('/countries', null, { enableCache: true, cacheKey: 'countries', cacheDurationMs: 60 * 60 * 1000 // 1 hour }); } getCurrencies(): Observable<CurrencyDto[]> { return this.get<CurrencyDto[]>('/currencies', null, { enableCache: true, cacheKey: 'currencies' }); } getTimezones(): Observable<TimezoneDto[]> { return this.get<TimezoneDto[]>('/timezones', null, { enableCache: true }); } // ───────────────────────────────────────────────────────────────────────── // CACHE INVALIDATION // ───────────────────────────────────────────────────────────────────────── invalidateCountriesCache(): void { this.clearCache('countries'); } invalidateAllCache(): void { this.clearAllCache(); } }
Pattern 3: File Upload/Download
@Injectable({ providedIn: 'root' }) export class DocumentApiService extends PlatformApiService { protected get apiUrl(): string { return environment.apiUrl + '/api/Document'; } // ───────────────────────────────────────────────────────────────────────── // FILE UPLOAD // ───────────────────────────────────────────────────────────────────────── upload(file: File, metadata?: DocumentMetadata): Observable<DocumentDto> { const formData = new FormData(); formData.append('file', file, file.name); if (metadata) { formData.append('metadata', JSON.stringify(metadata)); } return this.postFormData<DocumentDto>('/upload', formData); } uploadMultiple(files: File[]): Observable<DocumentDto[]> { const formData = new FormData(); files.forEach((file, index) => { formData.append(`files[${index}]`, file, file.name); }); return this.postFormData<DocumentDto[]>('/upload-multiple', formData); } // ───────────────────────────────────────────────────────────────────────── // FILE DOWNLOAD // ───────────────────────────────────────────────────────────────────────── download(id: string): Observable<Blob> { return this.getBlob(`/${id}/download`); } downloadAsBase64(id: string): Observable<string> { return this.get<string>(`/${id}/base64`); } // ───────────────────────────────────────────────────────────────────────── // HELPER: Trigger browser download // ───────────────────────────────────────────────────────────────────────── downloadAndSave(id: string, fileName: string): Observable<void> { return this.download(id).pipe( tap(blob => { const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = fileName; link.click(); window.URL.revokeObjectURL(url); }), map(() => void 0) ); } }
Pattern 4: API Service with Custom Headers
@Injectable({ providedIn: 'root' }) export class ExternalApiService extends PlatformApiService { protected get apiUrl(): string { return environment.externalApiUrl; } // Override to add custom headers protected override getDefaultHeaders(): HttpHeaders { return super.getDefaultHeaders().set('X-Api-Key', environment.externalApiKey).set('X-Request-Id', this.generateRequestId()); } // Method with custom headers getWithCustomHeaders(endpoint: string): Observable<any> { return this.get(endpoint, null, { headers: { 'X-Custom-Header': 'custom-value' } }); } private generateRequestId(): string { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } }
Pattern 5: Search/Autocomplete API
@Injectable({ providedIn: 'root' }) export class EmployeeApiService extends PlatformApiService { protected get apiUrl(): string { return environment.apiUrl + '/api/Employee'; } // ───────────────────────────────────────────────────────────────────────── // SEARCH WITH DEBOUNCE (use in component) // ───────────────────────────────────────────────────────────────────────── search(term: string): Observable<EmployeeDto[]> { if (!term || term.length < 2) { return of([]); } return this.get<EmployeeDto[]>('/search', { searchText: term, maxResultCount: 10 }); } // ───────────────────────────────────────────────────────────────────────── // AUTOCOMPLETE WITH CACHING // ───────────────────────────────────────────────────────────────────────── autocomplete(prefix: string): Observable<AutocompleteItem[]> { return this.get<AutocompleteItem[]>('/autocomplete', { prefix }, { enableCache: true, cacheKey: `autocomplete-${prefix}`, cacheDurationMs: 30 * 1000 // 30 seconds }); } } // Usage in component with debounce: @Component({...}) export class EmployeeSearchComponent { private searchSubject = new Subject<string>(); search$ = this.searchSubject.pipe( debounceTime(300), distinctUntilChanged(), switchMap(term => this.employeeApi.search(term)) ); onSearchInput(term: string): void { this.searchSubject.next(term); } }
Base PlatformApiService Methods
| Method | Purpose | Example |
|---|---|---|
| GET request | |
| POST request | |
| PUT request | |
| PATCH request | |
| DELETE request | |
| POST with FormData | |
| GET binary data | |
| Clear specific cache | |
| Clear all cache | |
Request Options
interface RequestOptions { // Caching enableCache?: boolean; cacheKey?: string; cacheDurationMs?: number; // Headers headers?: { [key: string]: string }; // Response handling responseType?: 'json' | 'text' | 'blob' | 'arraybuffer'; // Progress tracking reportProgress?: boolean; observe?: 'body' | 'events' | 'response'; }
Anti-Patterns to AVOID
:x: Using HttpClient directly
// WRONG - bypasses platform features constructor(private http: HttpClient) { } // CORRECT - extend PlatformApiService export class MyApiService extends PlatformApiService { }
:x: Hardcoding URLs
// WRONG return this.get('https://api.example.com/users'); // CORRECT - use environment protected get apiUrl() { return environment.apiUrl + '/api/User'; }
:x: Not handling errors in service
// WRONG - let errors propagate unhandled return this.get('/users'); // CORRECT - component handles via tapResponse this.userApi.getUsers().pipe( this.tapResponse( users => { /* success */ }, error => { /* handle error */ } ) );
:x: Missing type safety
// WRONG - returns any getUser(id: string) { return this.get(`/users/${id}`); } // CORRECT - typed response getUser(id: string): Observable<UserDto> { return this.get<UserDto>(`/users/${id}`); }
Verification Checklist
- Extends
PlatformApiService -
getter returns correct base URLapiUrl - All methods have return type annotations
- DTOs defined for request/response
- Caching configured for appropriate endpoints
- File operations use
/postFormDatagetBlob - Validation endpoints return
boolean -
for singleton@Injectable({ providedIn: 'root' })