Skip to main content

Overview

While ngrx-rtk-query includes a built-in fetchBaseQuery using the Fetch API, you can integrate Angular’s HttpClient to leverage interceptors, custom configurations, and existing HTTP infrastructure.

Why Use HttpClient?

Angular’s HttpClient provides:
  • Interceptors: Authentication tokens, error handling, logging
  • Request/Response transformations: Automatic JSON parsing
  • Progress events: Upload/download progress tracking
  • Test utilities: Built-in testing support with HttpTestingController
  • Type safety: Full TypeScript support
  • RxJS integration: Observable-based API

Basic HttpClient Integration

Create a custom base query that uses Angular’s HttpClient:
1

Create HttpClient base query

http-base-query.ts
import { inject } from '@angular/core';
import { HttpClient, type HttpErrorResponse } from '@angular/common/http';
import { lastValueFrom } from 'rxjs';
import { fetchBaseQuery } from 'ngrx-rtk-query';

export const httpClientBaseQuery = fetchBaseQuery(
  (http = inject(HttpClient)) => {
    return async (args, { signal }) => {
      const {
        url,
        method = 'GET',
        body = undefined,
        params = undefined,
      } = typeof args === 'string' ? { url: args } : args;

      try {
        const data = await lastValueFrom(
          http.request(method, url, {
            body,
            params,
            // Note: HttpClient doesn't support AbortSignal directly
          })
        );
        return { data };
      } catch (error) {
        const httpError = error as HttpErrorResponse;
        return {
          error: {
            status: httpError.status,
            data: httpError.message,
          },
        };
      }
    };
  }
);
2

Use in your API definition

api.ts
import { createApi } from 'ngrx-rtk-query';
import { httpClientBaseQuery } from './http-base-query';

export interface Post {
  id: number;
  name: string;
  content: string;
}

export const postsApi = createApi({
  reducerPath: 'postsApi',
  baseQuery: httpClientBaseQuery,
  tagTypes: ['Posts'],
  endpoints: (build) => ({
    getPosts: build.query<Post[], void>({
      query: () => ({ url: '/posts' }),
      providesTags: ['Posts'],
    }),
    addPost: build.mutation<Post, Partial<Post>>({
      query: (body) => ({
        url: '/posts',
        method: 'POST',
        body,
      }),
      invalidatesTags: ['Posts'],
    }),
  }),
});

export const { useGetPostsQuery, useAddPostMutation } = postsApi;
3

Provide HttpClient

app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideStoreApi } from 'ngrx-rtk-query';
import { postsApi } from './api';
import { authInterceptor } from './auth.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([authInterceptor])
    ),
    provideStore(),
    provideStoreApi(postsApi),
  ],
};
The base query uses inject() to access HttpClient within the Angular DI context.

With Base URL from Environment

Inject environment configuration for dynamic base URLs:
http-base-query.ts
import { inject } from '@angular/core';
import { HttpClient, type HttpErrorResponse } from '@angular/common/http';
import { lastValueFrom } from 'rxjs';
import { fetchBaseQuery } from 'ngrx-rtk-query';
import { ENVIRONMENT } from './environment.token';

export const httpClientBaseQuery = fetchBaseQuery(
  (
    http = inject(HttpClient),
    environment = inject(ENVIRONMENT)
  ) => {
    return async (args, { signal }) => {
      const {
        url,
        method = 'GET',
        body = undefined,
        params = undefined,
      } = typeof args === 'string' ? { url: args } : args;

      const fullUrl = `${environment.baseAPI}${url}`;

      try {
        const data = await lastValueFrom(
          http.request(method, fullUrl, { body, params })
        );
        return { data };
      } catch (error) {
        const httpError = error as HttpErrorResponse;
        return {
          error: {
            status: httpError.status,
            data: httpError.message,
          },
        };
      }
    };
  }
);
environment.token.ts
import { InjectionToken } from '@angular/core';

export interface Environment {
  baseAPI: string;
  production: boolean;
}

export const ENVIRONMENT = new InjectionToken<Environment>('ENVIRONMENT');

Authentication Interceptor

Add authentication tokens using HttpClient interceptors:
1

Create auth interceptor

auth.interceptor.ts
import { inject } from '@angular/core';
import { HttpInterceptorFn } from '@angular/common/http';
import { AuthService } from './auth.service';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);
  const token = authService.getToken();

  if (token) {
    const clonedRequest = req.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`,
      },
    });
    return next(clonedRequest);
  }

  return next(req);
};
2

Register interceptor

app.config.ts
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './auth.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([authInterceptor])
    ),
    provideStoreApi(postsApi),
  ],
};
All RTK Query requests will automatically include the authentication token.

Error Handling Interceptor

Handle errors globally with an interceptor:
error.interceptor.ts
import { inject } from '@angular/core';
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { catchError, throwError } from 'rxjs';
import { NotificationService } from './notification.service';

export const errorInterceptor: HttpInterceptorFn = (req, next) => {
  const notificationService = inject(NotificationService);

  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      let errorMessage = 'An error occurred';

      if (error.error instanceof ErrorEvent) {
        // Client-side error
        errorMessage = `Error: ${error.error.message}`;
      } else {
        // Server-side error
        errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;
      }

      notificationService.showError(errorMessage);
      return throwError(() => error);
    })
  );
};

Logging Interceptor

Log all HTTP requests and responses:
logging.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { tap } from 'rxjs';

export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
  const startTime = Date.now();
  console.log(`→ ${req.method} ${req.url}`);

  return next(req).pipe(
    tap({
      next: (event) => {
        if (event.type === 4) { // HttpResponse
          const duration = Date.now() - startTime;
          console.log(`← ${req.method} ${req.url} (${duration}ms)`);
        }
      },
      error: (error) => {
        const duration = Date.now() - startTime;
        console.error(`✗ ${req.method} ${req.url} (${duration}ms)`, error);
      },
    })
  );
};

Custom Headers

Add custom headers to specific endpoints:
export const postsApi = createApi({
  baseQuery: httpClientBaseQuery,
  endpoints: (build) => ({
    getPosts: build.query<Post[], void>({
      query: () => ({
        url: '/posts',
        headers: {
          'X-Custom-Header': 'value',
        },
      }),
    }),
  }),
});

Request Retry Logic

Implement automatic retry with exponential backoff:
retry.interceptor.ts
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { retry, timer } from 'rxjs';

export const retryInterceptor: HttpInterceptorFn = (req, next) => {
  return next(req).pipe(
    retry({
      count: 3,
      delay: (error: HttpErrorResponse, retryCount) => {
        // Don't retry 4xx errors (client errors)
        if (error.status >= 400 && error.status < 500) {
          throw error;
        }
        
        // Exponential backoff: 1s, 2s, 4s
        const delayMs = Math.pow(2, retryCount - 1) * 1000;
        console.log(`Retry attempt ${retryCount} after ${delayMs}ms`);
        return timer(delayMs);
      },
    })
  );
};

Testing with HttpClient

Test API calls with HttpTestingController:
posts.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { provideStore } from '@ngrx/store';
import { provideStoreApi } from 'ngrx-rtk-query';
import { postsApi, useGetPostsQuery } from './api';

describe('Posts API', () => {
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [
        provideStore(),
        provideStoreApi(postsApi),
      ],
    });

    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('should fetch posts', async () => {
    const mockPosts = [{ id: 1, name: 'Test Post', content: 'Content' }];
    
    const query = useGetPostsQuery();

    const req = httpMock.expectOne('/posts');
    expect(req.request.method).toBe('GET');
    req.flush(mockPosts);

    await new Promise(resolve => setTimeout(resolve, 100));
    expect(query.data()).toEqual(mockPosts);
  });
});

Complete Example

Here’s a complete example with multiple interceptors:
1

Create base query

http-base-query.ts
import { inject } from '@angular/core';
import { HttpClient, type HttpErrorResponse } from '@angular/common/http';
import { lastValueFrom } from 'rxjs';
import { fetchBaseQuery } from 'ngrx-rtk-query';

export const httpClientBaseQuery = fetchBaseQuery(
  (http = inject(HttpClient)) => {
    return async (args, api) => {
      const {
        url,
        method = 'GET',
        body = undefined,
        params = undefined,
      } = typeof args === 'string' ? { url: args } : args;

      try {
        const data = await lastValueFrom(
          http.request(method, url, { body, params })
        );
        return { data };
      } catch (error) {
        const httpError = error as HttpErrorResponse;
        return {
          error: {
            status: httpError.status,
            data: httpError.error || httpError.message,
          },
        };
      }
    };
  }
);
2

Create API

api.ts
import { createApi } from 'ngrx-rtk-query';
import { httpClientBaseQuery } from './http-base-query';

export const postsApi = createApi({
  reducerPath: 'postsApi',
  baseQuery: httpClientBaseQuery,
  tagTypes: ['Posts'],
  endpoints: (build) => ({
    getPosts: build.query<Post[], void>({
      query: () => '/posts',
    }),
  }),
});
3

Configure providers

app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideStoreApi } from 'ngrx-rtk-query';
import { postsApi } from './api';
import { authInterceptor } from './auth.interceptor';
import { errorInterceptor } from './error.interceptor';
import { loggingInterceptor } from './logging.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([
        authInterceptor,
        errorInterceptor,
        loggingInterceptor,
      ])
    ),
    provideStore(),
    provideStoreApi(postsApi),
  ],
};

Fetch vs HttpClient

FeatureFetch APIHttpClient
Bundle SizeSmallerLarger
Interceptors❌ No✅ Yes
Progress Events❌ No✅ Yes
RxJS Integration❌ No✅ Yes
Test UtilitiesLimited✅ HttpTestingController
Browser SupportModernAll
Setup ComplexitySimpleModerate

Best Practices

1

Centralize base query

Create a single base query file and reuse it across all APIs.
2

Use interceptors for cross-cutting concerns

Handle authentication, logging, and error handling in interceptors instead of base query.
3

Type your error responses

Define error interfaces for consistent error handling:
interface ApiError {
  status: number;
  message: string;
  details?: string[];
}
4

Leverage environment configuration

Use injection tokens for environment-specific values like base URLs.
5

Test with HttpTestingController

Use Angular’s built-in testing utilities for HTTP mocking.

Troubleshooting

HttpClient not found

Ensure provideHttpClient() is added to your providers.
providers: [
  provideHttpClient(), // Required
  provideStoreApi(postsApi),
]

Interceptor not running

Make sure interceptors are registered:
provideHttpClient(
  withInterceptors([authInterceptor, errorInterceptor])
)

CORS errors

CORS is configured on the server side. Ensure your API allows requests from your Angular app’s origin.

Build docs developers (and LLMs) love