Back to catalog

angular-services

Mejores prácticas para crear servicios Angular. Usa esto al crear servicios, manejar llamadas HTTP, gestionar estado o implementar lógica de negocio.

  • Category: discovered
  • Tags: github, community
  • Version: 1.0.0
  • Author: walid
  • Downloads: 0
  • Updated: 2025-11-22

Download the latest version (v1.0.0) from this page.

SKILL.md

---
description: Mejores prácticas para crear servicios Angular. Usa esto al crear servicios, manejar llamadas HTTP, gestionar estado o implementar lógica de negocio.
---

# Angular Services Best Practices 🔧

## Service Design Principles

Design services around a **single responsibility**. Each service should have one clear purpose.

## Service Creation

### Use `providedIn: 'root'`

Always use `providedIn: 'root'` for singleton services:

```typescript
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root' // Makes it a singleton available app-wide
})
export class UserService {
  // Service implementation
}
```

### Benefits of `providedIn: 'root'`

- ✅ Automatic tree-shaking (unused services removed from bundle)
- ✅ No need to add to providers array
- ✅ Singleton pattern automatically
- ✅ Available throughout the application

## Dependency Injection

### Use `inject()` Function

**ALWAYS** use the `inject()` function instead of constructor injection:

```typescript
import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  // ✅ Modern approach - use inject()
  private http = inject(HttpClient);
  private router = inject(Router);
  
  // ❌ Avoid constructor injection
  // constructor(
  //   private http: HttpClient,
  //   private router: Router
  // ) {}
}
```

### Why `inject()` is Better

- More concise and readable
- Better for functional programming style
- Works in functional contexts
- Easier to test
- Modern Angular pattern

## Service Structure

### Example Service Structure

```typescript
import { Injectable, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private http = inject(HttpClient);
  private apiUrl = '/api/users';
  
  // Optional: Local service state with signals
  private usersCache = signal<User[]>([]);
  
  // Public API methods
  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(this.apiUrl);
  }
  
  getUser(id: number): Observable<User> {
    return this.http.get<User>(`${this.apiUrl}/${id}`);
  }
  
  createUser(user: Partial<User>): Observable<User> {
    return this.http.post<User>(this.apiUrl, user);
  }
  
  updateUser(id: number, user: Partial<User>): Observable<User> {
    return this.http.patch<User>(`${this.apiUrl}/${id}`, user);
  }
  
  deleteUser(id: number): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/${id}`);
  }
}
```

## HTTP Operations

### Always Type HTTP Responses

```typescript
// ✅ Good - typed response
getUsers(): Observable<User[]> {
  return this.http.get<User[]>(this.apiUrl);
}

// ❌ Bad - untyped response
getUsers(): Observable<any> {
  return this.http.get(this.apiUrl);
}
```

### Handle Query Parameters

```typescript
getUsers(page: number, size: number, search?: string): Observable<UserResponse> {
  const params = new HttpParams()
    .set('page', page.toString())
    .set('size', size.toString());
  
  if (search) {
    params.set('search', search);
  }
  
  return this.http.get<UserResponse>(this.apiUrl, { params });
}
```

## Error Handling

Always handle HTTP errors in services using RxJS operators:

```typescript
import { catchError, throwError } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private http = inject(HttpClient);
  private messageService = inject(MessageService);
  
  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(this.apiUrl).pipe(
      catchError(error => {
        console.error('Error fetching users:', error);
        
        // Show user-friendly message
        this.messageService.add({
          severity: 'error',
          summary: 'Error',
          detail: 'Failed to load users. Please try again.'
        });
        
        // Re-throw or return empty array
        return throwError(() => error);
      })
    );
  }
}
```

## Service Responsibilities

### What Services Should Do

- ✅ Make HTTP requests
- ✅ Handle business logic
- ✅ Manage application state (when appropriate)
- ✅ Transform and validate data
- ✅ Communicate with backend APIs
- ✅ Provide reusable functionality

### What Services Should NOT Do

- ❌ Manipulate the DOM
- ❌ Handle presentation logic
- ❌ Manage component-specific state (use component signals instead)
- ❌ Import Angular component decorators

## Service Communication Patterns

### Service-to-Service Communication

Services can depend on other services:

```typescript
@Injectable({
  providedIn: 'root'
})
export class OrderService {
  private http = inject(HttpClient);
  private userService = inject(UserService); // Inject another service
  private companyContext = inject(CompanyContextService);
  
  createOrder(order: CreateOrderDto): Observable<Order> {
    const company = this.companyContext.selectedCompany();
    if (!company) {
      throw new Error('No company selected');
    }
    
    return this.http.post<Order>('/api/orders', {
      ...order,
      companyId: company.companyId
    });
  }
}
```

## Testing Services

Services using `inject()` are easier to test:

```typescript
describe('UserService', () => {
  let service: UserService;
  let httpMock: HttpTestingController;
  
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule]
    });
    
    service = TestBed.inject(UserService);
    httpMock = TestBed.inject(HttpTestingController);
  });
  
  it('should fetch users', () => {
    const mockUsers: User[] = [{ id: 1, name: 'John' }];
    
    service.getUsers().subscribe(users => {
      expect(users).toEqual(mockUsers);
    });
    
    const req = httpMock.expectOne('/api/users');
    expect(req.request.method).toBe('GET');
    req.flush(mockUsers);
  });
});
```

## ApiResponse Wrapper Pattern 📦

**CRITICAL**: All backend responses in this project come wrapped in an `ApiResponse` structure. **ALWAYS** unwrap the response using `.pipe(map())`.

### ApiResponse Structure

```typescript
interface ApiResponse<T> {
  success: boolean;
  message: string;
  data: T;
  error?: ApiError;
}

interface ApiError {
  title: string;
  message: string;
}
```

### Standard Pattern

```typescript
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, map } from 'rxjs';
import { environment } from '../../../environments/environment';

interface Biometric {
  biometricId: number;
  code: string;
  description: string;
  ip: string;
  macAddress: string;
  portNumber: number;
  status: boolean;
}

interface ApiResponse<T> {
  success: boolean;
  message: string;
  data: T;
  error?: {
    title: string;
    message: string;
  };
}

@Injectable({
  providedIn: 'root'
})
export class BiometricService {
  private readonly http = inject(HttpClient);
  private readonly apiUrl = `${environment.apiUrl}Biometric`;

  /**
   * Get all biometric devices for a specific company
   * Backend returns: { success: true, message: "...", data: Biometric[] }
   * We return: Biometric[]
   */
  getBiometricsByCompany(companyId: number): Observable<Biometric[]> {
    const url = `${this.apiUrl}/company/${companyId}`;
    return this.http.get<ApiResponse<Biometric[]>>(url).pipe(
      map(response => response.data || [])  // ✅ ALWAYS unwrap here
    );
  }

  /**
   * Get active biometric devices
   */
  getActiveBiometrics(): Observable<Biometric[]> {
    const url = `${this.apiUrl}/active`;
    return this.http.get<ApiResponse<Biometric[]>>(url).pipe(
      map(response => response.data || [])
    );
  }

  /**
   * Get single biometric device by ID
   */
  getBiometricById(id: number): Observable<Biometric> {
    const url = `${this.apiUrl}/${id}`;
    return this.http.get<ApiResponse<Biometric>>(url).pipe(
      map(response => response.data)  // Single item - no fallback needed
    );
  }

  /**
   * Create new biometric device
   */
  createBiometric(data: CreateBiometricCommand): Observable<Biometric> {
    return this.http.post<ApiResponse<Biometric>>(this.apiUrl, data).pipe(
      map(response => response.data)
    );
  }

  /**
   * Update existing biometric device
   */
  updateBiometric(id: number, data: UpdateBiometricCommand): Observable<Biometric> {
    const url = `${this.apiUrl}/${id}`;
    return this.http.put<ApiResponse<Biometric>>(url, data).pipe(
      map(response => response.data)
    );
  }

  /**
   * Toggle biometric device status
   */
  toggleBiometricStatus(id: number): Observable<Biometric> {
    const url = `${this.apiUrl}/${id}/toggle-status`;
    return this.http.patch<ApiResponse<Biometric>>(url, {}).pipe(
      map(response => response.data)
    );
  }
}
```

### Key Points

1. **Type the HTTP call with `ApiResponse<T>`**

   ```typescript
   this.http.get<ApiResponse<Biometric[]>>(url)
   ```

2. **Always use `.pipe(map())` to unwrap**

   ```typescript
   .pipe(
     map(response => response.data || [])  // Arrays
   )

   .pipe(
     map(response => response.data)  // Single items
   )
   ```

3. **Return the unwrapped type**

   ```typescript
   // Component receives the unwrapped data directly
   getBiometrics(): Observable<Biometric[]> {  // Not Observable<ApiResponse<Biometric[]>>
     return this.http.get<ApiResponse<Biometric[]>>(url).pipe(
       map(response => response.data || [])
     );
   }
   ```

### Dropdown Endpoints Pattern

Many endpoints provide simplified data for dropdowns:

```typescript
interface BiometricDropdownItem {
  value: number;
  text: string;
}

getBiometricsForDropdown(): Observable<BiometricDropdownItem[]> {
  const url = `${this.apiUrl}/dropdown`;
  return this.http.get<ApiResponse<BiometricDropdownItem[]>>(url).pipe(
    map(response => response.data || [])
  );
}
```

### Error Handling with ApiResponse

The `ApiResponse.error` field contains structured error information:

```typescript
import { catchError, throwError } from 'rxjs';

getBiometrics(): Observable<Biometric[]> {
  return this.http.get<ApiResponse<Biometric[]>>(this.apiUrl).pipe(
    map(response => {
      // Check if backend marked as unsuccessful
      if (!response.success && response.error) {
        console.error('Backend error:', response.error);
        throw new Error(response.error.message);
      }
      return response.data || [];
    }),
    catchError(error => {
      console.error('HTTP error:', error);
      this.messageService.add({
        severity: 'error',
        summary: 'Error',
        detail: 'No se pudieron cargar los datos'
      });
      return throwError(() => error);
    })
  );
}
```

### Common Mistake ❌

```typescript
// ❌ WRONG - Component receives wrapped response
getBiometrics(): Observable<ApiResponse<Biometric[]>> {
  return this.http.get<ApiResponse<Biometric[]>>(url);
}

// In component - BAD
this.service.getBiometrics().subscribe(response => {
  this.biometrics.set(response.data);  // Component shouldn't unwrap
});
```

### Correct Pattern ✅

```typescript
// ✅ CORRECT - Service unwraps, component receives clean data
getBiometrics(): Observable<Biometric[]> {
  return this.http.get<ApiResponse<Biometric[]>>(url).pipe(
    map(response => response.data || [])
  );
}

// In component - GOOD
this.service.getBiometrics().subscribe(biometrics => {
  this.biometrics.set(biometrics);  // Direct usage
});
```

### Real Examples

See implementations in:

- `src/app/features/biometrics/services/biometric.service.ts`
- `src/app/features/structures/services/companies.service.ts`
- `src/app/features/income/deductions/services/deduction.service.ts`

### Benefits of This Pattern

1. **Type Safety**: Components work with clean, unwrapped types
2. **Consistent**: All services follow the same pattern
3. **Maintainable**: If API structure changes, update in services only
4. **Testable**: Easy to mock unwrapped data in tests
5. **Clean Components**: Components don't deal with API wrapper structure

## Best Practices Summary

- ✅ Use `providedIn: 'root'` for singleton services
- ✅ Use `inject()` function instead of constructor injection
- ✅ Design services with single responsibility
- ✅ Always type HTTP responses
- ✅ **ALWAYS unwrap `ApiResponse` with `.pipe(map())`**
- ✅ Handle errors gracefully with `catchError`
- ✅ Log errors appropriately
- ✅ Show user-friendly error messages
- ✅ Keep services focused and testable
- ❌ Don't manipulate DOM in services
- ❌ Don't use `any` type for API responses
- ❌ Don't mix presentation logic with business logic
- ❌ **Don't return `ApiResponse<T>` from service methods** - unwrap first