Downloading Files from Ecommerce Backend
Some ecommerce backends expose endpoints that return binary data — for example, an invoice PDF, a shipping label, or an export file. Because Alokai's recommended pattern is to route all traffic through the middleware, you can't simply redirect the browser to the backend URL. Instead, you need to pass the binary data through the middleware layer to the frontend.
This guide shows you how to do that safely and correctly using base64 encoding.
How it works
HTTP responses from the ecommerce backend may contain raw binary data. JSON — the format used between the middleware and the SDK — can't represent binary data directly. The solution is a two-step encoding:
- Middleware: request the binary file with
responseType: 'arraybuffer', then re-encode it as a base64 string before returning it to the frontend. - Frontend: decode the base64 string back into binary, create a
Blob, and trigger the browser's built-in file download.
Step 1: Middleware endpoint
Create a custom API method that calls your ecommerce backend and returns the file as base64.
1
Define your types in apps/storefront-middleware/api/custom-methods/types.ts:
export interface DownloadInvoiceArgs {
userId: string;
orderId: string;
}
export interface DownloadInvoiceResponse {
invoice: string; // base64-encoded file content
}
2
Implement the method in apps/storefront-middleware/api/custom-methods/downloadInvoice.ts:
import type { IntegrationContext } from '@/types';
import type { DownloadInvoiceArgs, DownloadInvoiceResponse } from './types';
export async function downloadInvoice(
context: IntegrationContext,
{ userId, orderId }: DownloadInvoiceArgs,
): Promise<DownloadInvoiceResponse> {
const { api: { baseSiteId } } = context.config;
const response = await context.client.get(
`/${baseSiteId}/users/${userId}/orders/${orderId}/download-invoice`,
{ responseType: 'arraybuffer' },
);
return {
invoice: Buffer.from(response.data).toString('base64'),
};
}
The key change is responseType: 'arraybuffer'. Without it, Axios tries to parse the response as text or JSON and the binary content gets corrupted. Buffer.from(...).toString('base64') then re-encodes the raw bytes as a safe ASCII string.
3
Export the method in apps/storefront-middleware/api/custom-methods/index.ts:
export * from './downloadInvoice';
export * from './types';
The example above uses the SAP Commerce Cloud context (context.client is an Axios instance), but the same pattern applies to any integration — just call the relevant API client.
Step 2: Frontend
Once the middleware returns the base64 string, the frontend decodes it, builds a Blob, and triggers a download using a temporary <a> element.
async function downloadInvoice(orderId: string) {
const { invoice } = await sdk.customExtension.downloadInvoice({
userId: 'current',
orderId,
});
const bytes = Uint8Array.from(atob(invoice), (c) => c.charCodeAt(0));
const blob = new Blob([bytes], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `invoice-${orderId}.pdf`;
a.click();
URL.revokeObjectURL(url);
}
What the frontend code does
| Step | Code | Why |
|---|---|---|
| Decode base64 | atob(invoice) | Converts the ASCII string back to a binary string |
| Build byte array | Uint8Array.from(...) | Creates raw bytes that the Blob constructor expects |
| Create a Blob | new Blob([bytes], { type: '...' }) | Wraps the bytes in a file-like object the browser understands |
| Generate URL | URL.createObjectURL(blob) | Creates a temporary in-memory URL pointing to the Blob |
| Trigger download | a.click() | Simulates a link click; the browser's download attribute sets the filename |
| Clean up | URL.revokeObjectURL(url) | Frees the memory as soon as the download begins |
Call URL.revokeObjectURL(url) after triggering the download. Skipping this will leak memory, especially if users download multiple files in one session.
Handling different file types
The Blob type must match the content returned by your backend. Set it to the correct MIME type for the file you're downloading:
| File type | MIME type |
|---|---|
application/pdf | |
| CSV | text/csv |
| Excel (xlsx) | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
| ZIP | application/zip |
If your backend includes a Content-Type header in its response, you can forward it from the middleware:
export async function downloadInvoice(
context: IntegrationContext,
{ userId, orderId }: DownloadInvoiceArgs,
) {
const response = await context.client.get(/* ... */, { responseType: 'arraybuffer' });
return {
invoice: Buffer.from(response.data).toString('base64'),
contentType: response.headers['content-type'] ?? 'application/octet-stream',
};
}
Also update DownloadInvoiceResponse in types.ts to include the new field, otherwise TypeScript will complain about the return type:
export interface DownloadInvoiceResponse {
invoice: string; // base64-encoded file content
contentType: string;
}
Then destructure contentType alongside invoice on the frontend and use it when constructing the Blob:
const { invoice, contentType } = await sdk.customExtension.downloadInvoice({
userId: 'current',
orderId,
});
const bytes = Uint8Array.from(atob(invoice), (c) => c.charCodeAt(0));
const blob = new Blob([bytes], { type: contentType });
Related guides
- Extending the Middleware — learn how to register custom API methods
- Data Federation — combine data from multiple integrations in a single endpoint