service-architecture

For larger applications, it's recommended to encapsulate your API logic in services. Here's how to build a robust, reusable data layer similar to TanStack Query but for Angular.

Step 1: Create a Base HTTP Service

Create a reusable wrapper for your API calls.

import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class ApiService {
  private http = inject(HttpClient);
  private baseUrl = 'https://api.example.com';

  async get<T>(endpoint: string, params?: Record<string, any>): Promise<T> {
    const httpParams = new HttpParams({ fromObject: params });
    return firstValueFrom(this.http.get<T>(`${this.baseUrl}${endpoint}`, { params: httpParams }));
  }
  
  // ... post, put, delete methods
}

Step 2: Create Feature Services (Query Factories)

Instead of calling cachedResource in components directly with inline loaders, expose them as methods in a domain service. This keeps your components clean and logic reusable.

import { Injectable, inject, signal } from '@angular/core';
import { cachedResource } from 'ngx-cachr';
import { ApiService } from '../../core/api.service';
import { Product } from './product.model';

@Injectable({ providedIn: 'root' })
export class ProductService {
  private api = inject(ApiService);

  /**
   * Fetches a list of products.
   * Key: ['products', category]
   */
  getProducts(category: () => string | null) {
    return cachedResource(() => ({
      key: ['products', category()],
      // Only fetch if category is present
      loader: async () => {
        const cat = category();
        if (!cat) return []; 
        return this.api.get<Product[]>('/products', { category: cat });
      },
      ttl: 5 * 60 * 1000, // 5 minutes
      strategy: 'swr'
    }));
  }

  /**
   * Fetches a single product details.
   * Key: ['product', id]
   */
  getProduct(id: () => string) {
    return cachedResource(() => ({
      key: ['product', id()],
      loader: () => this.api.get<Product>(`/products/${id()}`),
      strategy: 'cache-first' // don't re-fetch if we have it in memory/storage
    }));
  }
}

Step 3: Consume in Components

Your components now become purely reactive views.

import { Component, inject, signal } from '@angular/core';
import { ProductService } from './product.service';

@Component({
  template: `
    <select #cat (change)="category.set(cat.value)">
      <option value="electronics">Electronics</option>
      <option value="books">Books</option>
    </select>

    @if (products.status() === 'loading') {
      <skeleton-loader />
    }
    
    @for (product of products.data(); track product.id) {
      <product-card [product]="product" />
    }
  `
})
export class ProductListComponent {
  private productService = inject(ProductService);
  
  category = signal('electronics');
  
  // This signal is now fully managed, cached, and reactive!
  products = this.productService.getProducts(this.category);
}