Claude Code Plugins

Community-maintained marketplace

Feedback

angular-api-service

@duc01226/EasyPlatform
2
0

Use when creating API services for backend communication with proper patterns for caching, error handling, and type safety.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name angular-api-service
description Use when creating API services for backend communication with proper patterns for caching, error handling, and type safety.

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
  • List all endpoints to implement
  • Determine caching requirements
  • Search existing services: grep "{Feature}ApiService" --include="*.ts"

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<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