Skip to content

Commit 089d70b

Browse files
authored
Merge pull request #15 from YoussefChahib/feat/add-blob-download-api
feat: Add file download support to ApiService
2 parents 9ff945c + 9543884 commit 089d70b

3 files changed

Lines changed: 94 additions & 4 deletions

File tree

projects/ngx-api-client/src/lib/api.service.spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,38 @@ describe('ApiService', () => {
460460
req.flush(emptyPage());
461461
});
462462
});
463+
464+
describe('file downloads', () => {
465+
it('issues a GET and returns the file as a Blob', () => {
466+
const blob = new Blob(['file content'], { type: 'application/pdf' });
467+
468+
api.download('/documents/001').subscribe();
469+
470+
const req = httpMock.expectOne(`${BASE_URL}/api/v1/documents/001`);
471+
472+
expect(req.request.method).toBe('GET');
473+
expect(req.request.responseType).toBe('blob');
474+
475+
req.flush(blob);
476+
});
477+
478+
it('issues a GET and returns the full HTTP response for a file download', () => {
479+
const blob = new Blob(['file content'], { type: 'application/pdf' });
480+
481+
api.downloadResponse('/documents/001').subscribe();
482+
483+
const req = httpMock.expectOne(`${BASE_URL}/api/v1/documents/001`);
484+
485+
expect(req.request.method).toBe('GET');
486+
expect(req.request.responseType).toBe('blob');
487+
488+
req.flush(blob, {
489+
headers: {
490+
'Content-Disposition': 'attachment; filename="document.pdf"',
491+
},
492+
});
493+
});
494+
});
463495

464496
describe('loading state', () => {
465497
it('does not start until the caller subscribes', () => {

projects/ngx-api-client/src/lib/api.service.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { HttpClient, HttpContext, HttpHeaders, HttpParams } from '@angular/common/http';
1+
import { HttpClient, HttpContext, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http';
22
import { inject, Injectable } from '@angular/core';
33
import { defer, finalize, Observable } from 'rxjs';
44
import { ApiLoadingService } from './api-loading.service';
@@ -99,6 +99,30 @@ export class ApiService {
9999
delete<T>(endpoint: string, options?: ApiRequestOptions): Observable<T> {
100100
return this.request<T>('DELETE', endpoint, null, options);
101101
}
102+
103+
/**
104+
* Downloads a binary file by performing a `GET` request to the specified API endpoint
105+
* with optional request options.
106+
*
107+
* @param endpoint API endpoint (e.g. `'/resource/{id}'`).
108+
* @param options Additional request options (query params, headers, etc.).
109+
* @returns An `Observable` of the downloaded file as a `Blob`.
110+
*/
111+
download(endpoint: string, options?: ApiRequestOptions): Observable<Blob> {
112+
return this.request<Blob>('GET', endpoint, null, options, 'blob');
113+
}
114+
115+
/**
116+
* Downloads a binary file and returns the complete HTTP response, including
117+
* response metadata such as headers and the filename from the `Content-Disposition` header.
118+
*
119+
* @param endpoint API endpoint (e.g. `'/resource/{id}'`).
120+
* @param options Additional request options (query params, headers, etc.).
121+
* @returns An `Observable` of the complete HTTP response containing the downloaded file as a `Blob`.
122+
*/
123+
downloadResponse(endpoint: string, options?: ApiRequestOptions): Observable<HttpResponse<Blob>> {
124+
return this.request<HttpResponse<Blob>>('GET', endpoint, null, options, 'blob', true);
125+
}
102126

103127
/**
104128
* Convenience method for paginated `GET` requests.
@@ -138,13 +162,16 @@ export class ApiService {
138162
* @param endpoint API endpoint (e.g. `'/resource'`).
139163
* @param body Request body (for POST, PUT, PATCH).
140164
* @param options Additional request options (query params, headers, flags for interceptors, etc.).
141-
* @returns An `Observable` of the response body typed as `T`.
165+
* @param observeResponse Whether to return the complete HTTP response, including headers and other metadata.
166+
* @returns An `Observable` of the response typed as `T`.
142167
*/
143168
private request<T>(
144169
method: string,
145170
endpoint: string,
146171
body: unknown,
147172
options?: ApiRequestOptions,
173+
responseType: 'json' | 'blob' = 'json',
174+
observeResponse = false,
148175
): Observable<T> {
149176
const showLoader = options?.showLoader ?? this.defaultShowLoader;
150177

@@ -157,12 +184,16 @@ export class ApiService {
157184
this.loadingService.start();
158185
}
159186

160-
return this.http.request<T>(method, url, {
187+
const requestOptions = {
161188
body,
162189
params: this.buildParams(options, versioning),
163190
headers: this.buildHeaders(options, versioning),
164191
context,
165-
});
192+
responseType,
193+
...(observeResponse ? { observe: 'response' as const } : {}),
194+
};
195+
196+
return this.http.request(method, url, requestOptions) as Observable<T>;
166197
}).pipe(
167198
finalize(() => {
168199
if (showLoader) {

website/docs/api/api-service.mdx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ The single entry point for HTTP calls. Builds URLs from the configured
1616
`HttpContext` for interceptors, and tracks loading state.
1717

1818
Every method returns an `Observable` of the **response body**, typed as `T`.
19+
Except for file download methods, they provide dedicated return types: `download()` returns a
20+
`Blob`, while `downloadResponse()` returns the complete `HttpResponse<Blob>`.
1921
Requests are created lazilynothing is sent until you subscribe.
2022

2123
## `get`
@@ -63,6 +65,31 @@ delete<T>(endpoint: string, options?: ApiRequestOptions): Observable<T>
6365
this.api.delete<void>(`/orders/${id}`);
6466
```
6567
68+
```ts
69+
download(endpoint: string, options?: ApiRequestOptions): Observable<Blob>
70+
```
71+
72+
Downloads a binary file and returns the response body as a `Blob`.
73+
74+
```ts
75+
this.api.download('/documents/001');
76+
```
77+
78+
## `downloadResponse`
79+
80+
```ts
81+
downloadResponse(
82+
endpoint: string,
83+
options?: ApiRequestOptions,
84+
): Observable<HttpResponse<Blob>>
85+
```
86+
87+
Downloads a binary file and returns the complete HTTP response, including response headers and metadata.
88+
89+
```ts
90+
this.api.downloadResponse('/documents/001');
91+
```
92+
6693
## `getPage`
6794
6895
```ts

0 commit comments

Comments
 (0)