angular-api-service

Angular API Service Development Workflow

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "angular-api-service" with this command: npx skills add congdon1207/agents.md/congdon1207-agents-md-angular-api-service

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/PlatformExampleAppWeb/apps/playground-text-snippet/docs/design-system/

Key docs to read:

  • README.md

  • Component overview, base classes, library summary

  • 07-technical-guide.md

  • Implementation checklist, best practices

  • 06-state-management.md

  • State management and API integration patterns

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&#x3C;PagedResult&#x3C;FeatureDto>> {
    return this.get&#x3C;PagedResult&#x3C;FeatureDto>>('', query);
}

getById(id: string): Observable&#x3C;FeatureDto> {
    return this.get&#x3C;FeatureDto>(`/${id}`);
}

getByCode(code: string): Observable&#x3C;FeatureDto> {
    return this.get&#x3C;FeatureDto>('/by-code', { code });
}

// ─────────────────────────────────────────────────────────────────────────
// COMMAND METHODS
// ─────────────────────────────────────────────────────────────────────────

save(command: SaveFeatureCommand): Observable&#x3C;FeatureDto> {
    return this.post&#x3C;FeatureDto>('', command);
}

update(id: string, command: Partial&#x3C;SaveFeatureCommand>): Observable&#x3C;FeatureDto> {
    return this.put&#x3C;FeatureDto>(`/${id}`, command);
}

delete(id: string): Observable&#x3C;void> {
    return this.deleteRequest&#x3C;void>(`/${id}`);
}

// ─────────────────────────────────────────────────────────────────────────
// VALIDATION METHODS
// ─────────────────────────────────────────────────────────────────────────

checkCodeExists(code: string, excludeId?: string): Observable&#x3C;boolean> {
    return this.get&#x3C;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&#x3C;CountryDto[]> {
    return this.get&#x3C;CountryDto[]>('/countries', null, {
        enableCache: true,
        cacheKey: 'countries',
        cacheDurationMs: 60 * 60 * 1000 // 1 hour
    });
}

getCurrencies(): Observable&#x3C;CurrencyDto[]> {
    return this.get&#x3C;CurrencyDto[]>('/currencies', null, {
        enableCache: true,
        cacheKey: 'currencies'
    });
}

getTimezones(): Observable&#x3C;TimezoneDto[]> {
    return this.get&#x3C;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&#x3C;DocumentDto> {
    const formData = new FormData();
    formData.append('file', file, file.name);

    if (metadata) {
        formData.append('metadata', JSON.stringify(metadata));
    }

    return this.postFormData&#x3C;DocumentDto>('/upload', formData);
}

uploadMultiple(files: File[]): Observable&#x3C;DocumentDto[]> {
    const formData = new FormData();
    files.forEach((file, index) => {
        formData.append(`files[${index}]`, file, file.name);
    });

    return this.postFormData&#x3C;DocumentDto[]>('/upload-multiple', formData);
}

// ─────────────────────────────────────────────────────────────────────────
// FILE DOWNLOAD
// ─────────────────────────────────────────────────────────────────────────

download(id: string): Observable&#x3C;Blob> {
    return this.getBlob(`/${id}/download`);
}

downloadAsBase64(id: string): Observable&#x3C;string> {
    return this.get&#x3C;string>(`/${id}/base64`);
}

// ─────────────────────────────────────────────────────────────────────────
// HELPER: Trigger browser download
// ─────────────────────────────────────────────────────────────────────────

downloadAndSave(id: string, fileName: string): Observable&#x3C;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&#x3C;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&#x3C;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

  • apiUrl getter 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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

frontend-development

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

clean-code

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

code-review

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

devops

No summary provided by upstream source.

Repository SourceNeeds Review