Skip to content

Commit 68e3884

Browse files
authored
docs: Add file download/blob examples for RestEndpoint.parseResponse (#3818)
Document how to override parseResponse for binary responses (blob, arrayBuffer) including Content-Disposition header parsing and browser download trigger pattern. Made-with: Cursor
1 parent 781fafc commit 68e3884

File tree

3 files changed

+120
-2
lines changed

3 files changed

+120
-2
lines changed

.cursor/skills/data-client-rest/SKILL.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: data-client-rest
3-
description: Define REST APIs with @data-client/rest - resource(), RestEndpoint, CRUD, GET/POST/PUT/DELETE, HTTP fetch, normalize, cache, urlPrefix, path parameters
3+
description: Define REST APIs with @data-client/rest - resource(), RestEndpoint, CRUD, GET/POST/PUT/DELETE, HTTP fetch, normalize, cache, urlPrefix, path parameters, file download, blob, parseResponse
44
license: Apache 2.0
55
---
66
# Guide: Using `@data-client/rest` for Resource Modeling
@@ -134,6 +134,30 @@ getOptimisticResponse(snap, { id }) {
134134
- **url(urlParams):** `urlPrefix` + `path` + (`searchParams``searchToString()`)
135135
- **getRequestInit(body):** `getHeaders()` + `method` + `signal`
136136

137+
#### Non-JSON responses (file download, blob, arrayBuffer)
138+
139+
Override `parseResponse()` for binary/non-JSON responses. Set `schema: undefined` (not normalizable) and `dataExpiryLength: 0` to avoid caching large blobs.
140+
141+
```ts
142+
const downloadFile = new RestEndpoint({
143+
path: '/files/:id/download',
144+
schema: undefined,
145+
dataExpiryLength: 0,
146+
async parseResponse(response) {
147+
const blob = await response.blob();
148+
const disposition = response.headers.get('Content-Disposition');
149+
const filename =
150+
disposition?.match(/filename="?(.+?)"?$/)?.[1] ?? 'download';
151+
return { blob, filename };
152+
},
153+
process(value): { blob: Blob; filename: string } {
154+
return value;
155+
},
156+
});
157+
```
158+
159+
For complete usage with browser download trigger, see [network-transform: file download](references/network-transform.md#file-download).
160+
137161
---
138162

139163
## 5. **Extending Resources**

docs/rest/api/RestEndpoint.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -783,7 +783,49 @@ on ['content-type' header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Hea
783783

784784
If `status` is 204, resolves as `null`.
785785

786-
Override this to handle other response types like [arrayBuffer](https://developer.mozilla.org/en-US/docs/Web/API/Response/arrayBuffer)
786+
Override this to handle other response types like [blob](https://developer.mozilla.org/en-US/docs/Web/API/Response/blob) or [arrayBuffer](https://developer.mozilla.org/en-US/docs/Web/API/Response/arrayBuffer).
787+
788+
#### File downloads {#file-download}
789+
790+
For binary responses like file downloads, override `parseResponse` to use `response.blob()`.
791+
Set `schema: undefined` since binary data is not normalizable. Use `dataExpiryLength: 0` to
792+
avoid caching large blobs in memory.
793+
794+
```ts
795+
const downloadFile = new RestEndpoint({
796+
path: '/files/:id/download',
797+
schema: undefined,
798+
dataExpiryLength: 0,
799+
parseResponse(response) {
800+
return response.blob();
801+
},
802+
process(blob): { blob: Blob; filename: string } {
803+
return { blob, filename: 'download' };
804+
},
805+
});
806+
```
807+
808+
To extract the filename from the `Content-Disposition` header, override both `parseResponse` and `process`:
809+
810+
```ts
811+
const downloadFile = new RestEndpoint({
812+
path: '/files/:id/download',
813+
schema: undefined,
814+
dataExpiryLength: 0,
815+
async parseResponse(response) {
816+
const blob = await response.blob();
817+
const disposition = response.headers.get('Content-Disposition');
818+
const filename =
819+
disposition?.match(/filename="?(.+?)"?$/)?.[1] ?? 'download';
820+
return { blob, filename };
821+
},
822+
process(value): { blob: Blob; filename: string } {
823+
return value;
824+
},
825+
});
826+
```
827+
828+
See [file download guide](../guides/network-transform.md#file-download) for complete usage with browser download trigger.
787829

788830
### process(value, ...args): any {#process}
789831

docs/rest/guides/network-transform.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,58 @@ class GithubEndpoint<
294294
}
295295
```
296296

297+
## File download {#file-download}
298+
299+
For endpoints that return binary data (files, images, PDFs), override
300+
[parseResponse](../api/RestEndpoint.md#parseResponse) to call
301+
[response.blob()](https://developer.mozilla.org/en-US/docs/Web/API/Response/blob) instead of the
302+
default JSON/text parsing. Set `schema: undefined` since binary data isn't normalizable, and
303+
`dataExpiryLength: 0` to avoid caching large blobs in memory.
304+
305+
```typescript title="downloadFile.ts"
306+
import { RestEndpoint } from '@data-client/rest';
307+
308+
const downloadFile = new RestEndpoint({
309+
path: '/files/:id/download',
310+
schema: undefined,
311+
dataExpiryLength: 0,
312+
async parseResponse(response) {
313+
const blob = await response.blob();
314+
const disposition = response.headers.get('Content-Disposition');
315+
const filename =
316+
disposition?.match(/filename="?(.+?)"?$/)?.[1] ?? 'download';
317+
return { blob, filename };
318+
},
319+
process(value): { blob: Blob; filename: string } {
320+
return value;
321+
},
322+
});
323+
```
324+
325+
```tsx title="DownloadButton.tsx"
326+
import { useController } from '@data-client/react';
327+
import { downloadFile } from './downloadFile';
328+
329+
function DownloadButton({ id }: { id: string }) {
330+
const ctrl = useController();
331+
332+
const handleDownload = async () => {
333+
const { blob, filename } = await ctrl.fetch(downloadFile, { id });
334+
const url = URL.createObjectURL(blob);
335+
const a = document.createElement('a');
336+
a.href = url;
337+
a.download = filename;
338+
a.click();
339+
URL.revokeObjectURL(url);
340+
};
341+
342+
return <button onClick={handleDownload}>Download</button>;
343+
}
344+
```
345+
346+
For `ArrayBuffer` responses (useful for processing binary data in-memory), use
347+
`response.arrayBuffer()` the same way.
348+
297349
## Name calling
298350

299351
Sometimes an API might change a key name, or choose one you don't like. Of course

0 commit comments

Comments
 (0)