Vue Storefront is now Alokai! Learn More
Downloading Files from Ecommerce Backend

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:

  1. Middleware: request the binary file with responseType: 'arraybuffer', then re-encode it as a base64 string before returning it to the frontend.
  2. 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:

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:

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:

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

StepCodeWhy
Decode base64atob(invoice)Converts the ASCII string back to a binary string
Build byte arrayUint8Array.from(...)Creates raw bytes that the Blob constructor expects
Create a Blobnew Blob([bytes], { type: '...' })Wraps the bytes in a file-like object the browser understands
Generate URLURL.createObjectURL(blob)Creates a temporary in-memory URL pointing to the Blob
Trigger downloada.click()Simulates a link click; the browser's download attribute sets the filename
Clean upURL.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 typeMIME type
PDFapplication/pdf
CSVtext/csv
Excel (xlsx)application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
ZIPapplication/zip

If your backend includes a Content-Type header in its response, you can forward it from the middleware:

downloadInvoice.ts
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:

types.ts
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 });