DEV Community

mohammad garmabi
mohammad garmabi

Posted on Originally published at mohammad-garmabi.hashnode.dev

Angular API Requests with tanstack-fetch and TanStack Query

Angular API Requests with tanstack-fetch and TanStack Query

How do you keep HTTP requests consistent across an Angular application without mixing API transport, server-state management, and UI logic?

One practical option is to use a dedicated HTTP client alongside TanStack Query.

In this tutorial, we'll build a small Angular users screen using tanstack-fetch.

tanstack-fetch is an independent open-source project by Mohammad Garmabi, not an official TanStack package.

Install

npm install tanstack-fetch
npm install @tanstack/angular-query-experimental
Enter fullscreen mode Exit fullscreen mode

Create the 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',
});
Enter fullscreen mode Exit fullscreen mode

Provide it through Angular DI

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 }),
  ],
});
Enter fullscreen mode Exit fullscreen mode

Query data and pass AbortSignal

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...</p>
    } @else if (users.isError()) {
      <p>{{ errorLabel() }}</p>
    } @else {
      @for (user of users.data(); track user.id) {
        <p>{{ user.name }}</p>
      }
    }
  `,
})
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}`
      : 'Request failed';
  }
}
Enter fullscreen mode Exit fullscreen mode

What's happening here?

There are three distinct responsibilities:

  1. Angular DI provides the configured HTTP client.
  2. tanstack-fetch handles the HTTP request and typed response.
  3. TanStack Query manages query state and caching.

Passing signal matters. A query can be cancelled as its lifecycle changes, and forwarding the AbortSignal lets the HTTP request participate in that cancellation.

Error handling is also explicit: isFetchError() identifies errors produced by the HTTP client, allowing the UI to display an HTTP status where available.

What about authentication and retries?

Treat these as separate design decisions. Configure authentication, retry behavior, and application-specific error policies according to your security and reliability requirements. Don't assume every HTTP error should be retried, and don't automatically treat every authorization response as a reason to log a user out.

When is this useful?

Consider this architecture when your Angular app has multiple API endpoints, shared HTTP configuration, typed data contracts, and a need for consistent cancellation and error handling.

For a small application, Angular's built-in HttpClient may be enough. An additional HTTP client is valuable only when its abstractions solve a real problem in your codebase.

References

What does your Angular application use for API requests: HttpClient, a custom wrapper, or a dedicated fetch client?


Author: Mohammad Garmabi — creator of tanstack-fetch, an independent TypeScript HTTP client.

Top comments (0)