Skip to content

shopware/frontends - api-client

shopware/frontends - api-client

Dynamic and fully typed API Client for Shopware 6. Usable in any JavaScript and TypeScript project. You can use types generated from your custom API instance to have autocompletion and type safety.

To generate your own types use @shopware/api-gen CLI.

Setup

Install npm package:

sh
# ✨ Auto-detect
npx nypm install @shopware/api-client

# npm
npm install @shopware/api-client

# yarn
yarn add @shopware/api-client

# pnpm
pnpm add @shopware/api-client

# bun
bun install @shopware/api-client

# deno
deno install npm:@shopware/api-client

Store API client setup

Recommended practice is to create a separate module file. For example src/apiClient.ts, and import it whenever you need to use API Client.

typescript
import { createAPIClient } from "@shopware/api-client";

// You can pick types of your current API version, the default one:
import type { operations } from "@shopware/api-client/store-api-types";
// or - RECOMMENDED - your types generated by [@shopware/api-gen](https://www.npmjs.com/package/@shopware/api-gen) CLI:
import type { operations } from "./api-types/storeApiTypes";

// you can pick cookies library of your choice
import Cookies from "js-cookie";

export const apiClient = createAPIClient<operations>({
  baseURL: "https://demo-frontends.shopware.store/store-api",
  accessToken: "SWSCBHFSNTVMAWNZDNFKSHLAYW",
  contextToken: Cookies.get("sw-context-token"),
});

apiClient.hook("onContextChanged", (newContextToken) => {
  Cookies.set("sw-context-token", newContextToken, {
    expires: 365, // days
    path: "/",
    sameSite: "lax",
    secure: shopwareEndpoint.startsWith("https://"),
  });
});

Admin API client setup

typescript
import { createAdminAPIClient } from "@shopware/api-client";

The setup works the same way as creteAPIClient function, with few differences

credentials (optional) - Quick scripting or token-based authentication

We provide optional credentials parameter to createAdminAPIClient. Which allows you to use authentication type of your choice whenever you wish to create connection to any endpoint.

Example:

typescript
import type {
  operations,
} from "@shopware/api-client/admin-api-types"; // we take default admin api types from different directory than store-api - use your own types by generating schema with @shopware/api-gen CLI
import type { operations } from "./api-types/adminApiTypes"; // or use your own types generated by @shopware/api-gen CLI

const adminApiClient = createAdminAPIClient<operations>({
  baseURL: `${process.env.SHOP_URL}/api`,
  credentials: {
    grant_type: "password",
    client_id: "administration",
    scope: "write",
    username: process.env.SHOP_ADMIN_USERNAME,
    password: process.env.SHOP_ADMIN_PASSWORD,
  },
  // credentials: { // or token-based example
  //   grant_type: "client_credentials",
  //   client_id: "administration",
  //   client_secret: process.env.SHOP_ADMIN_TOKEN,
  // },
});

await adminApiClient.invoke(...); // invoke defined endpoint

sessionData (optional) - Persistent authentication

This parameter is used to store session data in cookies (or other place you want to store it), so you can keep your session persistent.

You can combine this option with credentials property.

typescript
// example adminApiClient.ts file
import { createAdminAPIClient } from "@shopware/api-client"; // we use different function to create admin api client

import { createAdminAPIClient } from "@shopware/api-client";
import type { operations, Schemas } from "@shopware/api-client/admin-api-types"; // we take default admin api types from different directory than store-api
import Cookies from "js-cookie";

export const adminApiClient = createAdminAPIClient<operations>({
  baseURL: "https://demo-frontends.shopware.store/api",
  sessionData: JSON.parse(Cookies.get("sw-admin-session-data") || "{}"),
});

adminApiClient.hook("onAuthChange", (sessionData) => {
  Cookies.set("sw-admin-session-data", JSON.stringify(sessionData), {
    expires: 1, // days
    path: "/",
    sameSite: "lax",
    secure: shopwareEndpoint.startsWith("https://"),
  });
});

the rest works the same as store-api client.

Customizing API Types

The client is fully typed via a generic operations parameter. You can use the bundled default types or generate and override types from your own Shopware instance.

Generating types from your instance

Use @shopware/api-gen to generate TypeScript types directly from your Shopware instance's OpenAPI schema.

bash
# 1. Load the schema from your running Shopware instance
pnpx @shopware/api-gen loadSchema --apiType=store

# 2. Generate TypeScript types
pnpx @shopware/api-gen generate --apiType=store

This creates api-types/storeApiTypes.ts (or adminApiTypes.ts for Admin API). Point shopware.d.ts to your generated types instead of the bundled defaults:

typescript
// shopware.d.ts
declare module "#shopware" {
  import type { createAPIClient } from "@shopware/api-client";

  export type operations = import("./api-types/storeApiTypes").operations;
  export type Schemas =
    import("./api-types/storeApiTypes").components["schemas"];
  export type ApiClient = ReturnType<typeof createAPIClient<operations>>;
}

All code importing from #shopware will now use your instance's types automatically.

Add a script to package.json to make regeneration easy:

json
{
  "scripts": {
    "generate-types": "shopware-api-gen generate --apiType=store"
  }
}

TypeScript overrides

If your instance has custom fields, custom endpoints, or incorrect types in the OpenAPI spec, you can override or extend the generated types without modifying the generated file directly.

Create an overrides file next to the generated types:

  • api-types/storeApiTypes.overrides.ts — for Store API
  • api-types/adminApiTypes.overrides.ts — for Admin API

Create api-types/storeApiTypes.overrides.ts with your merged types:

typescript
// api-types/storeApiTypes.overrides.ts
import type { components as mainComponents } from "./storeApiTypes";

// Extend schemas with your custom fields
export type components = mainComponents & {
  schemas: Schemas;
};

export type Schemas = {
  // Fully override an existing schema
  Product: mainComponents["schemas"]["Product"] & {
    customFields: {
      my_custom_field: string;
    };
  };
};

// Add or override operations
export type operations = {
  // Add a custom endpoint
  "myCustomEndpoint post /custom/endpoint": {
    contentType?: "application/json";
    accept?: "application/json";
    body: { id: string };
    response: components["schemas"]["Product"];
    responseCode: 200;
  };
  // Override an existing operation (e.g. restrict the request body)
  "updateCustomerAddress patch /account/address/{addressId}": {
    contentType?: "application/json";
    accept?: "application/json";
    body: { city: string };
    response: components["schemas"]["CustomerAddress"];
    responseCode: 200;
  };
};

IMPORTANT

Overriding a schema or operation requires a full object definition — partial overrides are not supported in TypeScript overlay files.

Then point shopware.d.ts to the overrides file instead of the generated one:

typescript
// shopware.d.ts
declare module "#shopware" {
  import type { createAPIClient } from "@shopware/api-client";

  export type operations =
    import("./api-types/storeApiTypes.overrides").operations;
  export type Schemas =
    import("./api-types/storeApiTypes.overrides").components["schemas"];
  export type ApiClient = ReturnType<typeof createAPIClient<operations>>;
}

Your apiClient.ts already imports from #shopware, so no change is needed there — the overridden types flow through automatically.

JSON patch overrides (partial schema fixes)

For fine-grained, field-level corrections to the JSON schema (e.g. marking a field as required, fixing a wrong type), use patch files. These are applied before TypeScript generation and support partial changes.

Create a storeApiTypes.overrides.json patch file:

json
{
  "components": {
    "Cart": {
      "required": ["price", "errors"]
    }
  }
}

Reference it in api-gen.config.json:

json
{
  "$schema": "./node_modules/@shopware/api-gen/api-gen.schema.json",
  "store-api": {
    "patches": [
      "storeApiSchema.overrides.json",
      "./api-types/myCustomPatches.json"
    ]
  }
}

See the @shopware/api-gen documentation for the full patching reference and available configuration options.

Basic usage

Take a look at example project using API Client.

Simple invocation

typescript
import { apiClient, RequestReturnType } from "./apiClient";

// could be reactive value, you can use ApiReturnType to type it properly
let productsResponse: RequestReturnType<"readProduct">;

async function loadProducts() {
  productsResponse = await apiClient.invoke("readProduct post /product", {
    limit: 2,
  });
}

Fetch features

The new API client is leveraging ofetch library, which has built in support for AbortController, timeout and other features.

Example usage of AbortController to cancell your request:

typescript
const controller = new AbortController();

const request = client.invoke("readContext get /context", {
  fetchOptions: {
    signal: controller.signal,
  },
});

controller.abort(); // At this point client will throw an error with the information, that the request has been cancelled

Other example of using fetchOptions for setting the timeout:

typescript
const request = client.invoke("readContext get /context", {
  fetchOptions: {
    timeout: 5000, // 5 seconds
  },
});

All exposed options available under fetchOptions are:

  • cache
  • duplex
  • keepalive
  • priority
  • redirect
  • retry
  • retryDelay
  • retryStatusCodes
  • signal
  • timeout

Predefining methods

If you prefer to add another layer of abstraction you can use created previously types to define your own concept of methods.

typescript
// add for example into apiClient.ts file
const readNavigation = ({
  depth,
  type,
}: {
  depth: number;
  type: "main-navigation";
}) =>
  apiClient.invoke("readNavigation post /navigation/{activeId}/{rootId}", {
    headers: {
      "sw-include-seo-urls": true,
    },
    pathParams: {
      activeId: type,
      rootId: type,
    },
    body: {
      depth,
    },
  });

// in another file you can use it, and depth property will be set to 2 by default
import { readNavigation } from "./apiClient";

async function loadMainNavigation() {
  const navigation = await readNavigation({
    body: { activeId: "main-navigation", rootId: "main-navigation" },
  });
}

Uploading files (multipart/form-data) and other binary bodies

Some endpoints accept binary uploads sent as multipart/form-data - for example the Admin API uploadV2 post /_action/media/upload. For these requests, build a FormData instance and pass it as body:

typescript
const formData = new FormData();
formData.append("file", file); // a `File` or `Blob`, e.g. from an <input type="file">
formData.append("fileName", "my-image");

await adminApiClient.invoke("uploadV2 post /_action/media/upload", {
  // `contentType` / `accept` are type-level metadata on this operation and are
  // ignored at runtime - the request Content-Type is derived from the body
  contentType: "multipart/form-data",
  accept: "application/json",
  // pass the FormData directly; the typed object shape is only for guidance
  body: formData as unknown as { file: Blob },
});

IMPORTANT

Do not set the Content-Type header yourself, and pass a real body object, not a plain JSON object.

A multipart/form-data request must carry a unique boundary parameter (Content-Type: multipart/form-data; boundary=...). Only the runtime - the browser's fetch or undici on the server - can generate it, and only when no Content-Type is present. A hard-coded Content-Type: multipart/form-data has no boundary, so the server cannot parse the payload.

How the client handles this for you:

  • The client seeds a default Content-Type: application/json on every request. When the body is one the runtime must type itself - FormData, Blob/File, URLSearchParams, ArrayBuffer/typed arrays, or a stream - the client removes that default, so the body is never mislabelled as JSON. This applies to both the Store and Admin clients, in the browser and on the server.
  • What ends up on the wire then depends on the body. fetch/undici add a Content-Type only for FormData (multipart/form-data with a generated boundary), URLSearchParams (application/x-www-form-urlencoded), and a Blob/File that has a type. For ArrayBuffer/typed arrays, streams, and a typeless Blob the request is sent with no Content-Type at all. If the endpoint needs one, set it yourself.
  • A Content-Type you set explicitly is preserved - per request via headers, or client-wide via defaultHeaders.apply({ "Content-Type": "application/octet-stream" }).
  • Two cases override that, because keeping the header would break the request: a boundary-less multipart/form-data, and any boundary-less Content-Type on a FormData body. The runtime generates the boundary and passes it to the server only through the Content-Type, so a header without one leaves the server with bytes it cannot split - the upload then fails silently rather than erroring.
  • Always pass a real FormData / Blob / stream. A plain object is serialized to JSON ({ file: Blob } becomes {"file":{}}), which is not a valid upload.
  • The generated operation types describe the body as a plain object (e.g. { file: Blob }) for discoverability. At runtime you must provide the real body, so a cast like body: formData as unknown as <BodyType> may be required depending on your setup.

Error handling

Client is throwing ApiClientError with detailed information returned from the API. It will display clear message in the console or you can access details property to get raw information from the response.

typescript
import { ApiClientError } from "@shopware/api-client";

try {
  // ... your request
} catch (error) {
  if (error instanceof ApiClientError) {
    console.error(error); // This prints message summary
    console.error("Details:", error.details); // Raw response from API
  } else {
    console.error("==>", error); // Another type of error, not recognized by API client
  }
}

Hooks

Api client provides hooks to listen to events like context change, authentication change or default headers change. Example:

typescript
apiClient.hook("onDefaultHeaderChanged", (key, value) => {
  // here we can detect that the default header has changed, either by the user or by the headers incoming from the API
});

Available hooks:

  • onContextChanged: Triggered when context token changes
  • onResponseError: Triggered when API returns an error
  • onSuccessResponse: Triggered when API request succeeds
  • onDefaultHeaderChanged: Triggered when default headers are modified
  • onRequest: Triggered before each request is made, allowing for request inspection and modification

calling apiClient.hook will autocomplete the list of available hooks.

Base Configuration Management

The API client provides methods to manage its base configuration:

typescript
// Get current configuration
const config = apiClient.getBaseConfig();
console.log(config.baseURL); // "https://demo-frontends.shopware.store/store-api"
console.log(config.accessToken); // "SWSCBHFSNTVMAWNZDNFKSHLAYW"

// Update configuration
apiClient.updateBaseConfig({
  baseURL: "https://new-url.com/store-api",
  accessToken: "NEW_TOKEN",
});

This allows you to dynamically change the API endpoint or access token during runtime, for example when switching between different environments or when the access token needs to be updated.

Helper Functions

The API client provides helper functions that can be imported separately to keep your main bundle size smaller.

encodeForQuery

The encodeForQuery function compresses and encodes objects into base64url format for use in query strings. This is particularly useful for complex criteria objects that need to be passed as URL parameters.

Related issue: https://github.com/shopware/shopware/issues/12388

typescript
import { encodeForQuery } from "@shopware/api-client/helpers";

// Example: Encoding complex search criteria
const criteria = {
  page: 1,
  limit: 10,
  filter: [
    {
      type: "equals",
      field: "active",
      value: true,
    },
    {
      type: "contains",
      field: "name",
      value: "smartphone",
    },
  ],
  associations: {
    manufacturer: {},
    categories: {
      associations: {
        media: {},
      },
    },
  },
};

// Use in URL
apiClient.invoke("getProducts get /product", {
  query: {
    _criteria: encodeForQuery(encodedCriteria),
  },
});

Changelog

Full changelog for stable version is available here

Latest changes: 1.5.1

Patch Changes

  • #2600 b767721 Thanks @patzick! - Stop adopting sw-context-token from publicly cacheable Store API responses. CDN hits for cacheableReads GETs can replay a guest token from when the entry was stored, which overwrote the logged-in session and logged users out when navigating to account pages (e.g. before account/newsletter-recipient).

  • #2515 978b02c Thanks @patzick! - Generate customFields properties with a dedicated CustomFields type instead of the broader GenericRecord type.

  • #2522 33facb1 Thanks @mkucmus! - Fix file uploads and other binary requests. The client no longer forces the default Content-Type: application/json onto FormData, Blob/File, URLSearchParams, or binary/stream bodies, so the runtime can set the right content type itself (e.g. multipart/form-data with a boundary). Just pass the body and leave Content-Type alone.

    On a FormData body the client now also drops a Content-Type you set yourself when it carries no boundary. Only the runtime knows the boundary, and it reaches the server through that header, so keeping the header made the upload arrive as unparseable bytes with no error.

  • #2554 9137475 Thanks @patzick! - Make _criteria query encoding deterministic by pinning the gzip timestamp.

  • #2526 474d3fe Thanks @mkucmus! - Split the createAPIClient tests so Node and browser behavior are each tested in the right environment:

    • Node (createApiClient.test.ts): keeps the multipart Content-Type, aborts with This operation was aborted.
    • Browser (createApiClient.browser.test.ts, runs in happy-dom): drops the multipart Content-Type, aborts with signal is aborted without reason.

    Previously a stray @vitest-environment comment ran the whole suite in browser mode, so the Node paths were never actually checked.

API

createAPIClient

ts
export function createAPIClient<
  // TODO: Keep this broad until generated operation types are narrowed.
  OPERATIONS extends Record<string, any> = operations,
  PATHS extends string | number | symbol = keyof OPERATIONS,
>(params: {
  baseURL?: string;
  accessToken?: string;
  contextToken?: string;
  defaultHeaders?: ClientHeaders;
  fetchOptions?: GlobalFetchOptions;
})

source code

createAdminAPIClient

ts
export function createAdminAPIClient<
  // TODO: Keep this broad until generated operation types are narrowed.
  OPERATIONS extends Record<string, any> = operations,
  PATHS extends string | number | symbol = keyof OPERATIONS,
>(params: {
  baseURL?: string;
  /**
   * If you pass `credentials` object, it will be used to authenticate the client whenever session expires.
   * You don't need to manually invoke `/token` endpoint first.
   */
  credentials?: OPERATIONS["token post /oauth/token"]["body"];
  sessionData?: AdminSessionData;
  defaultHeaders?: ClientHeaders;
  fetchOptions?: GlobalFetchOptions;
})

source code

ApiError

ts
export type ApiError = {
  title?: string;
  detail?: string;
  code?: string;
  status?: string;
  source?: {
    pointer?: string;
  };
  meta?: {
    parameters?: Record<string, string> | [];
  };
};

source code

Was this page helpful?
UnsatisfiedSatisfied
Be the first to vote!
0.0 / 5  (0 votes)