Skip to main content

Command Palette

Search for a command to run...

A Better Angular API Layer: tanstack-fetch, Dependency Injection, and TanStack Query

Updated
•5 min read•View as Markdown

Description: Learn how to build a reusable Angular HTTP client with tanstack-fetch, TypeScript, Angular Dependency Injection, TanStack Query, and request cancellation.

Angular provides the building blocks for scalable applications, but a growing API layer can still become difficult to maintain.

Requests may be scattered across components. Error handling may differ between features. Cancellation may be forgotten. And API configuration can become tightly coupled to the UI.

A useful architectural principle is to separate three responsibilities:

  • HTTP transport

  • Server-state management

  • UI and dependency management

In this article, we'll implement that separation using Angular, tanstack-fetch, and TanStack Query.

Understanding the architecture

Before writing code, define the responsibilities of each layer.

Layer Responsibility
Angular Components and Dependency Injection
tanstack-fetch HTTP requests and typed HTTP errors
TanStack Query Query lifecycle and server-state caching

These tools complement one another rather than replacing one another.

tanstack-fetch is an independent TypeScript HTTP client created by Mohammad Garmabi. It is not an official TanStack package.

Step 1: Install the packages

npm install tanstack-fetch
npm install @tanstack/angular-query-experimental

The integration shown here follows the project's Angular documentation. Check the package compatibility requirements for your specific Angular version before adopting it.

Step 2: Define a shared API client

// api.ts
import { createFetch } from 'tanstack-fetch';

export interface User {
  id: number;
  name: string;
  email: string;
}

export const api = createFetch({
  baseUrl: 'https://jsonplaceholder.typicode.com',
});

The client is configured once and can be provided through Angular's dependency injection system.

This keeps the API base URL and client configuration out of individual components.

Step 3: Register the client

// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideFetchClient } from 'tanstack-fetch/angular';

import { api } from './api';
import { UsersComponent } from './users.component';

bootstrapApplication(UsersComponent, {
  providers: [
    provideFetchClient({ client: api }),
  ],
});

The application now has a configured HTTP client available through the Angular integration.

Step 4: Implement a query

// users.component.ts
import { Component } from '@angular/core';
import { injectQuery } from '@tanstack/angular-query-experimental';
import { injectFetch } from 'tanstack-fetch/angular';
import { isFetchError } from 'tanstack-fetch';

import { User } from './api';

@Component({
  selector: 'app-users',
  standalone: true,
  template: `
    @if (users.isPending()) {
      <p>Loading users...</p>
    } @else if (users.isError()) {
      <p>{{ errorLabel() }}</p>
    } @else {
      <ul>
        @for (user of users.data(); track user.id) {
          <li>{{ user.name }} ({{ user.email }})</li>
        }
      </ul>
    }
  `,
})
export class UsersComponent {
  private readonly api = injectFetch();

  readonly users = injectQuery(() => ({
    queryKey: ['users'],
    queryFn: ({ signal }: { signal: AbortSignal }) =>
      this.api.get<User[]>('/users', { signal }),
  }));

  errorLabel(): string {
    const error = this.users.error();

    return isFetchError(error)
      ? `HTTP ${error.status}: ${error.message}`
      : 'Unable to load users';
  }
}

Notice that the query function does not create a client, manually maintain loading state, or implement its own cache.

Instead, it delegates the HTTP request to the injected client and allows TanStack Query to manage the query lifecycle.

Why pass AbortSignal?

Cancellation is an important part of request lifecycle management.

Consider a component whose query is no longer needed. When cancellation is initiated by the query lifecycle, forwarding its AbortSignal gives the HTTP request a way to respond.

queryFn: ({ signal }: { signal: AbortSignal }) =>
  this.api.get<User[]>('/users', { signal })

This is more deliberate than launching an asynchronous request without connecting it to the query's cancellation mechanism.

Actual cancellation behavior depends on the HTTP client propagating the signal and the request implementation honoring it.

Error handling without unnecessary coupling

The example uses isFetchError() to distinguish client errors from other exceptions.

const error = this.users.error();

return isFetchError(error)
  ? `HTTP ${error.status}: ${error.message}`
  : 'Unable to load users';

This is a small but useful boundary between transport-level errors and UI presentation.

For a production application, you may want to standardize error mapping, authentication policies, logging, and retry behavior separately.

In particular, a 403 Forbidden response should not automatically be treated as an expired session. Authorization failures and authentication failures have different meanings and should be handled according to your application's security model.

When should you introduce a dedicated HTTP client?

A dedicated client is worth evaluating when:

  • Multiple features share the same API configuration.

  • Type-safe responses are important.

  • Query cancellation needs to reach the HTTP layer.

  • HTTP error handling needs consistent conventions.

  • You want to separate transport concerns from server-state caching.

However, abstraction has a cost. Angular's native HttpClient is a reasonable choice when it already meets your requirements. The goal is not to add dependencies; it is to reduce accidental complexity.

Conclusion

A maintainable Angular API layer starts with clear responsibilities.

Angular provides dependency management, tanstack-fetch provides the HTTP transport layer, and TanStack Query handles server-state management.

Together, they offer a practical architecture for typed, cancellation-aware API requests without mixing every concern into the component.

Documentation: https://mohamadgarmabi.github.io/tanstack-fetch/guide/angular

Source code: https://github.com/mohamadgarmabi/tanstack-fetch

Author: Mohammad Garmabi, software engineer and creator of tanstack-fetch.