Stop repeating HttpClient boilerplate in Angular

작성자

카테고리:

← 피드로
DEV Community · Rene Arias · 2026-09-24 개발(SW)

Set your API base URL and headers once with provideApi(), then inject(ApiService) anywhere. A tiny typed wrapper for Angular 17+.

Every Angular app that talks to a REST API ends up with the same few lines in every service: read the base URL from the environment, build the headers, glue the URL together, call HttpClient. Then the app grows, a second backend shows up, and those lines multiply.

I got tired of writing them, so years ago I wrote a small wrapper for my own apps. I just released version 3.0 of it, rebuilt for modern Angular: @arxis/api.

The problem

This is what a typical service looks like with HttpClient:

@Injectable({ providedIn: 'root' })
export class UserService {
  private readonly http = inject(HttpClient);
  private readonly baseUrl = environment.apiUrl;
  private readonly headers = new HttpHeaders({ 'X-Api-Key': environment.apiKey });

  getUsers() {
    return this.http.get<User[]>(`${this.baseUrl}/users`, { headers: this.headers });
  }

  createUser(body: CreateUserDto) {
    return this.http.post<User>(`${this.baseUrl}/users`, body, { headers: this.headers });
  }
}

Enter fullscreen mode Exit fullscreen mode

Only one part of each method is about users: 'users'. Everything else is plumbing, repeated in every service.

Configure the API once

npm install @arxis/api

Enter fullscreen mode Exit fullscreen mode

Tell the app where your API lives and which headers go with every request:

// app.config.ts
import { provideHttpClient } from '@angular/common/http';
import { provideApi } from '@arxis/api';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(),
    provideApi({
      url: environment.apiUrl,
      globalHeaders: { 'X-Api-Key': environment.apiKey },
    }),
  ],
};

Enter fullscreen mode Exit fullscreen mode

Now each service only describes its endpoints:

import { ApiService } from '@arxis/api';

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

  getUsers() {
    return this.api.get<User[]>('users');
  }

  createUser(body: CreateUserDto) {
    return this.api.post<User>('users', body);
  }
}

Enter fullscreen mode Exit fullscreen mode

ApiService has get, post, put, patch and delete. They’re typed, they accept query params as a plain object, and they have the same observe: 'response' and observe: 'events' overloads as HttpClient:

api.get<User[]>('users', { role: 'admin' });                      // GET /users?role=admin
api.get<User[]>('users', null, { observe: 'response' });          // full HttpResponse
api.post('upload', formData, { observe: 'events', reportProgress: true });

Enter fullscreen mode Exit fullscreen mode

It’s still your HttpClient

@arxis/api doesn’t create its own HTTP stack. It uses the HttpClient you configure, so everything you already have keeps working: interceptors, withFetch(), server-side rendering and HttpTestingController.

For example, an auth token that changes while the app runs belongs in an interceptor, and it applies to every ApiService request:

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = inject(AuthService).token();
  return next(token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req);
};

// app.config.ts
provideHttpClient(withInterceptors([authInterceptor])),
provideApi({ url: environment.apiUrl }),

Enter fullscreen mode Exit fullscreen mode

More than one backend

Extend ApiService once per API and pass its config to super():

@Injectable({ providedIn: 'root' })
export class BillingApiService extends ApiService {
  constructor() {
    super({ url: environment.billingUrl });
  }
}

Enter fullscreen mode Exit fullscreen mode

Or give a lazy-loaded part of the app its own base URL:

{
  path: 'admin',
  providers: [provideApi({ url: environment.adminApiUrl })],
  loadChildren: () => import('./admin/admin.routes'),
}

Enter fullscreen mode Exit fullscreen mode

Testing

Since requests go through Angular’s HttpClient, you test your services the usual way:

TestBed.configureTestingModule({
  providers: [
    provideHttpClient(),
    provideHttpClientTesting(),
    provideApi({ url: 'https://api.example.com' }),
  ],
});

TestBed.inject(UserService).getUsers().subscribe();
TestBed.inject(HttpTestingController).expectOne('https://api.example.com/users').flush([]);

Enter fullscreen mode Exit fullscreen mode

What’s new in 3.0

  • Works with Angular 17 through 22.
  • ApiService is providedIn: 'root' and uses inject().
  • provideApi() returns EnvironmentProviders, like provideHttpClient() and provideRouter().
  • If you forget provideApi(), the error tells you what to add.
  • About 1 KB minified and gzipped.

Coming from 1.x? The migration guide has the six steps.

Try it

If it saves you some boilerplate, a star on GitHub helps other Angular developers find it. Issues and ideas are welcome too.

원문에서 계속 ↗