Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion .cursor/skills/data-client-rest/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: data-client-rest
description: Define REST APIs with @data-client/rest - resource(), RestEndpoint, CRUD, GET/POST/PUT/DELETE, HTTP fetch, normalize, cache, urlPrefix, path parameters
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
license: Apache 2.0
---
# Guide: Using `@data-client/rest` for Resource Modeling
Expand Down Expand Up @@ -134,6 +134,30 @@ getOptimisticResponse(snap, { id }) {
- **url(urlParams):** `urlPrefix` + `path` + (`searchParams` → `searchToString()`)
- **getRequestInit(body):** `getHeaders()` + `method` + `signal`

#### Non-JSON responses (file download, blob, arrayBuffer)

Override `parseResponse()` for binary/non-JSON responses. Set `schema: undefined` (not normalizable) and `dataExpiryLength: 0` to avoid caching large blobs.

```ts
const downloadFile = new RestEndpoint({
path: '/files/:id/download',
schema: undefined,
dataExpiryLength: 0,
async parseResponse(response) {
const blob = await response.blob();
const disposition = response.headers.get('Content-Disposition');
const filename =
disposition?.match(/filename="?(.+?)"?$/)?.[1] ?? 'download';
return { blob, filename };
},
process(value): { blob: Blob; filename: string } {
return value;
},
});
```

For complete usage with browser download trigger, see [network-transform: file download](references/network-transform.md#file-download).

---

## 5. **Extending Resources**
Expand Down
44 changes: 43 additions & 1 deletion docs/rest/api/RestEndpoint.md
Original file line number Diff line number Diff line change
Expand Up @@ -783,7 +783,49 @@ on ['content-type' header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Hea

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

Override this to handle other response types like [arrayBuffer](https://developer.mozilla.org/en-US/docs/Web/API/Response/arrayBuffer)
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).

#### File downloads {#file-download}

For binary responses like file downloads, override `parseResponse` to use `response.blob()`.
Set `schema: undefined` since binary data is not normalizable. Use `dataExpiryLength: 0` to
avoid caching large blobs in memory.

```ts
const downloadFile = new RestEndpoint({
path: '/files/:id/download',
schema: undefined,
dataExpiryLength: 0,
parseResponse(response) {
return response.blob();
},
process(blob): { blob: Blob; filename: string } {
return { blob, filename: 'download' };
},
});
```

To extract the filename from the `Content-Disposition` header, override both `parseResponse` and `process`:

```ts
const downloadFile = new RestEndpoint({
path: '/files/:id/download',
schema: undefined,
dataExpiryLength: 0,
async parseResponse(response) {
const blob = await response.blob();
const disposition = response.headers.get('Content-Disposition');
const filename =
disposition?.match(/filename="?(.+?)"?$/)?.[1] ?? 'download';
return { blob, filename };
},
process(value): { blob: Blob; filename: string } {
return value;
},
});
```

See [file download guide](../guides/network-transform.md#file-download) for complete usage with browser download trigger.

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

Expand Down
52 changes: 52 additions & 0 deletions docs/rest/guides/network-transform.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,58 @@ class GithubEndpoint<
}
```

## File download {#file-download}

For endpoints that return binary data (files, images, PDFs), override
[parseResponse](../api/RestEndpoint.md#parseResponse) to call
[response.blob()](https://developer.mozilla.org/en-US/docs/Web/API/Response/blob) instead of the
default JSON/text parsing. Set `schema: undefined` since binary data isn't normalizable, and
`dataExpiryLength: 0` to avoid caching large blobs in memory.

```typescript title="downloadFile.ts"
import { RestEndpoint } from '@data-client/rest';

const downloadFile = new RestEndpoint({
path: '/files/:id/download',
schema: undefined,
dataExpiryLength: 0,
async parseResponse(response) {
const blob = await response.blob();
const disposition = response.headers.get('Content-Disposition');
const filename =
disposition?.match(/filename="?(.+?)"?$/)?.[1] ?? 'download';
return { blob, filename };
},
process(value): { blob: Blob; filename: string } {
return value;
},
});
```

```tsx title="DownloadButton.tsx"
import { useController } from '@data-client/react';
import { downloadFile } from './downloadFile';

function DownloadButton({ id }: { id: string }) {
const ctrl = useController();

const handleDownload = async () => {
const { blob, filename } = await ctrl.fetch(downloadFile, { id });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
};

return <button onClick={handleDownload}>Download</button>;
}
```

For `ArrayBuffer` responses (useful for processing binary data in-memory), use
`response.arrayBuffer()` the same way.

## Name calling

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