HTTP Service Wrapper
The Angular counterpart to HTTP Service Wrapper (Blazor): one generic ApiService every feature service calls through, so error handling and the ApiResponse<T> envelope (see ApiResponse & Controllers) are handled in exactly one place. The authInterceptor from the previous page already attaches the token, so this service never touches auth directly.
The envelope, mirrored in TypeScript
api-response.ts
export interface ApiResponse<T> {
value: T | null;
succeeded: boolean;
message: string | null;
}
Generic wrapper over HttpClient
api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, catchError, of } from 'rxjs';
import { ApiResponse } from './api-response';
import { environment } from '../environments/environment';
@Injectable({ providedIn: 'root' })
export class ApiService {
private readonly baseUrl = environment.apiBaseUrl;
constructor(private http: HttpClient) {}
get<T>(url: string): Observable<ApiResponse<T>> {
return this.http
.get<ApiResponse<T>>(`${this.baseUrl}${url}`)
.pipe(catchError((err) => this.handleError<T>(err)));
}
post<T>(url: string, body: unknown): Observable<ApiResponse<T>> {
return this.http
.post<ApiResponse<T>>(`${this.baseUrl}${url}`, body)
.pipe(catchError((err) => this.handleError<T>(err)));
}
put<T>(url: string, body: unknown): Observable<ApiResponse<T>> {
return this.http
.put<ApiResponse<T>>(`${this.baseUrl}${url}`, body)
.pipe(catchError((err) => this.handleError<T>(err)));
}
delete<T>(url: string): Observable<ApiResponse<T>> {
return this.http
.delete<ApiResponse<T>>(`${this.baseUrl}${url}`)
.pipe(catchError((err) => this.handleError<T>(err)));
}
postFormData<T>(url: string, formData: FormData): Observable<ApiResponse<T>> {
return this.http
.post<ApiResponse<T>>(`${this.baseUrl}${url}`, formData)
.pipe(catchError((err) => this.handleError<T>(err)));
}
private handleError<T>(error: unknown): Observable<ApiResponse<T>> {
const message =
error instanceof Object && 'error' in error
? (error as any).error?.message ?? 'An unexpected error occurred.'
: 'An unexpected error occurred.';
return of({ value: null, succeeded: false, message });
}
}
File upload — the Angular equivalent of Blazor's reflection-based multipart
No reflection needed in TypeScript — FormData is naturally a bag of string/Blob entries, so building it is direct rather than needing the property-walking trick from the Blazor page.
usage — building FormData
uploadResource(name: string, image: File): Observable<ApiResponse<void>> {
const formData = new FormData();
formData.append('name', name);
formData.append('image', image, image.name);
return this.api.postFormData<void>('/resource', formData);
}
A feature service on top
Same idea as the Blazor page's ResourceService — thin, one line per endpoint, all the error handling already lives in ApiService.
resource.service.ts
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { ApiService } from './api.service';
import { ApiResponse } from './api-response';
import { ResourceDto } from './resource.dto';
@Injectable({ providedIn: 'root' })
export class ResourceService {
constructor(private api: ApiService) {}
getAll(): Observable<ApiResponse<ResourceDto[]>> {
return this.api.get<ResourceDto[]>('/resource');
}
create(request: { name: string }): Observable<ApiResponse<object>> {
return this.api.post<object>('/resource', request);
}
}
Because every method returns anApiResponse<T>— including the caught-error case — a component's subscribe callback never needs a separateerror:handler; it checksresponse.succeededthe same way whether the request failed at the network layer or the API returnedsucceeded: falseon purpose.