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 });
}
}
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
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 },
}),
],
};
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);
}
}
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 });
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 }),
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 });
}
}
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'),
}
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([]);
What's new in 3.0
- Works with Angular 17 through 22.
-
ApiServiceisprovidedIn: 'root'and usesinject(). -
provideApi()returnsEnvironmentProviders, likeprovideHttpClient()andprovideRouter(). - 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
- Live example: open it in StackBlitz
- GitHub: renearias/arxis
- npm: @arxis/api
If it saves you some boilerplate, a star on GitHub helps other Angular developers find it. Issues and ideas are welcome too.
Top comments (0)