Agent skill
frontend-angular-api-service
Use when creating API services for backend communication with proper patterns for caching, error handling, and type safety.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/frontend-angular-api-service-duc01226-easyplatform-020e7920
SKILL.md
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 | docs/design-system/ |
| TextSnippetClient | src/Frontend/apps/playground-text-snippet/docs/design-system/ |
Key docs to read:
README.md- Component overview, base classes, library summary07-technical-guide.md- Implementation checklist, best practices06-state-management.md- State management and API integration patterns
File Location
src/Frontend/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<T>() |
GET request | this.get<User>('/users/1') |
post<T>() |
POST request | this.post<User>('/users', data) |
put<T>() |
PUT request | this.put<User>('/users/1', data) |
patch<T>() |
PATCH request | this.patch<User>('/users/1', partial) |
deleteRequest<T>() |
DELETE request | this.deleteRequest('/users/1') |
postFormData<T>() |
POST with FormData | this.postFormData('/upload', formData) |
getBlob() |
GET binary data | this.getBlob('/file/download') |
clearCache() |
Clear specific cache | this.clearCache('cacheKey') |
clearAllCache() |
Clear all cache | this.clearAllCache() |
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 -
apiUrlgetter returns correct base URL - All methods have return type annotations
- DTOs defined for request/response
- Caching configured for appropriate endpoints
- File operations use
postFormData/getBlob - Validation endpoints return
boolean -
@Injectable({ providedIn: 'root' })for singleton
Task Planning Notes
- Always plan and break many small todo tasks
- Always add a final review todo task to review the works done at the end to find any fix or enhancement needed
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?