> ## Documentation Index
> Fetch the complete documentation index at: https://docs.uselayers.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Upgrade guide: v2 to v3

> Migrate from @commerce-blocks/sdk v2 to v3: renamed client, stateless controllers, filter operator changes, simplified storage, and new controllers.

## Overview

v3 is a ground-up rewrite. The client is renamed back to `sdk`, controllers are stateless (params live at creation, overrides go through `get()`), Preact signals are gone, errors are plain `Error`, and facets and sort orders are fetched at runtime instead of declared at init.

This guide covers every breaking change with before/after snippets. For an end-to-end reference of the new surface, see the [SDK API reference](/sdk/api-reference).

## Install

```bash theme={null}
npm install @commerce-blocks/sdk@^3.0.0
```

The package name is unchanged. Subpath exports are unchanged:

```typescript theme={null}
import { createSDK, getSDK, isInitialized, resetSDK } from '@commerce-blocks/sdk'
import { subscribe } from '@commerce-blocks/sdk/sdk'
import { filter, and, or, eq, neq, between } from '@commerce-blocks/sdk/filter'
import { createDebounce } from '@commerce-blocks/sdk/utils'
import { xhrFetch } from '@commerce-blocks/sdk/xhr'
```

## Quick reference

| v2                       | v3                                                        |
| ------------------------ | --------------------------------------------------------- |
| `createClient(config)`   | `createSDK(config)` — returns `{ data, error }`           |
| `getClient()`            | `getSDK()` — returns `{ data, error }`                    |
| `Client`                 | Return type of `createSDK().data`                         |
| `ClientConfig`           | `SdkConfig`                                               |
| `token`                  | `storefrontAccessToken`                                   |
| —                        | `trackingToken` (new, required)                           |
| `client.suggest()`       | `sdk.autocomplete()` (or deprecated `sdk.suggest()`)      |
| `client.searchByImage()` | `sdk.imageSearch()`                                       |
| `client.cache`           | Removed — cache is internal                               |
| `controller.execute()`   | `controller.get()` (`execute` deprecated)                 |
| `state.value` (Signal)   | `controller.state` (plain getter)                         |
| `controller.getState()`  | `controller.state` (`getState()` deprecated)              |
| `subscribe(signal, cb)`  | `subscribe(controller, cb)` or `controller.subscribe(cb)` |
| `state.isFetching`       | `state.loading` (`isFetching` deprecated alias)           |
| `ClientError` hierarchy  | Plain `Error`                                             |
| `localStorageAdapter()`  | Pass `localStorage` directly                              |
| `StorageAdapter`         | `StorageBackend`                                          |
| `Result<T, E>`           | `ApiResult<T>` (`{ data, error }`)                        |

## Initialization

`createSDK()` returns `{ data, error }` — check the error before using the SDK.

<CodeGroup>
  ```typescript v2 theme={null}
  import { createClient, getClient } from '@commerce-blocks/sdk'

  const { data: client, error } = createClient({
    token: 'your-token',
    currency: 'USD',
    sorts: [{ name: 'Featured', code: 'featured' }],
    facets: [{ name: 'Color', code: 'options.color' }],
  })
  ```

  ```typescript v3 theme={null}
  import { createSDK, getSDK, isInitialized, resetSDK } from '@commerce-blocks/sdk'

  const { data: sdk, error } = createSDK({
    storefrontAccessToken: 'your-storefront-token',
    trackingToken: 'your-tracking-token',
    currencyCode: 'USD',
  })

  if (error) throw error
  ```
</CodeGroup>

### Config changes

| v2 `ClientConfig` | v3 `SdkConfig`          | Notes                                             |
| ----------------- | ----------------------- | ------------------------------------------------- |
| `token`           | `storefrontAccessToken` | Renamed                                           |
| —                 | `trackingToken`         | New — required for analytics                      |
| `currency`        | `currencyCode`          | Renamed                                           |
| `formatPrice`     | `formatPrice`           | Same `(amount, currencyCode) => string` signature |
| `swatches`        | `swatches`              | Unchanged                                         |
| `cacheLifetime`   | `cache.ttl`             | Nested                                            |
| `cacheLimit`      | `cache.maxSize`         | Nested                                            |
| `storage`         | `storage`               | New interface (see [Storage](#storage))           |
| `transforms`      | `transforms`            | Different keys and signatures (see below)         |
| `sorts`           | —                       | Fetch at runtime with `sdk.sortOrders()`          |
| `facets`          | —                       | Fetch at runtime with `sdk.facets()`              |
| `context`         | —                       | Pass per-request via controller params            |
| `attributes`      | —                       | Pass per-request via controller params            |
| `includeMeta`     | —                       | `_meta` is always included                        |
| `filterAliases`   | —                       | Removed                                           |
| `initialData`     | —                       | Removed                                           |
| `restoreCache`    | —                       | Removed                                           |

### Transforms

Endpoint transforms now receive the normalized response only — no separate `raw` argument. The `product` extender keeps its `{ base, raw }` signature. The `block` key is now plural, and `filters` was removed.

<CodeGroup>
  ```typescript v2 theme={null}
  transforms: {
    product: ({ base, raw }) => ({ description: raw.body_html ?? '' }),
    collection: (result, raw) => result,
    search: (result, raw) => result,
    block: (result, raw) => result,
    filters: (filters) => filters,
  }
  ```

  ```typescript v3 theme={null}
  transforms: {
    product: ({ base, raw }) => ({ description: raw.body_html ?? '' }),
    collection: (data) => data,
    search: (data) => data,
    blocks: (data) => data,
    similarProducts: (data) => data,
    imageSearch: (data) => data,
    searchContent: (data) => data,
    autocomplete: (data) => data,
    facets: (data) => data,
    sortOrders: (data) => data,
  }
  ```
</CodeGroup>

## Controllers

### Stateless params

Identity params (handle, blockId, query) go at controller creation. Query params (page, sort, filters) go at `get()` and shallow-merge over the init values. Nothing accumulates between calls.

<CodeGroup>
  ```typescript v2 theme={null}
  const search = client.search()
  await search.execute({ query: 'shoes', sort: 'relevance' })
  await search.execute({ page: 2 }) // query persists from the previous call
  ```

  ```typescript v3 theme={null}
  const search = sdk.search({ query: 'shoes' })
  await search.get()                     // uses init params
  await search.get({ sort: 'price_asc' }) // shallow-merges an override
  ```
</CodeGroup>

### State access

Preact signals are gone. `controller.state` is a plain getter and `subscribe` takes the controller directly.

<CodeGroup>
  ```typescript v2 theme={null}
  search.state.value                     // ReadonlySignal<QueryState>
  subscribe(search.state, (state) => {}) // signal-based
  ```

  ```typescript v3 theme={null}
  search.state                            // QueryState (plain object)
  subscribe(search, (state) => {})        // controller-based
  search.subscribe((state) => {})         // or the instance method
  ```
</CodeGroup>

`getState()` still works as a deprecated alias for `state`.

### QueryState shape

```diff theme={null}
  interface QueryState<T> {
    data: T | null
-   error: ClientError | null
+   error: Error | null
-   isFetching: boolean
+   loading: boolean
+   /** @deprecated */ isFetching: boolean  // alias for loading
  }
```

### `execute()` → `get()`

`execute()` still works but is deprecated:

```diff theme={null}
- await controller.execute({ ... })
+ await controller.get({ ... })
```

### Per-request body transform

All controllers accept `transformBody` at init to modify the request body before it is sent. This replaces v2's per-call `transformRequest`.

```typescript theme={null}
const collection = sdk.collection({
  collectionHandle: 'all',
  transformBody: (body) => ({ ...body, custom: true }),
})
```

## Controller-by-controller migration

### collection

<CodeGroup>
  ```typescript v2 theme={null}
  const col = client.collection({ handle: 'all' })
  await col.execute({ sort: 'price-asc', linking: { tag: 'sale' } })
  ```

  ```typescript v3 theme={null}
  const col = sdk.collection({ collectionHandle: 'all' })
  await col.get({
    sort: 'price-asc',
    dynamicLinking: { products: [{ productId: '123' }] },
  })
  ```
</CodeGroup>

| v2                 | v3                 |
| ------------------ | ------------------ |
| `handle`           | `collectionHandle` |
| `linking`          | `dynamicLinking`   |
| `transformRequest` | `transformBody`    |

### search

<CodeGroup>
  ```typescript v2 theme={null}
  const search = client.search()
  await search.execute({ query: 'shoes' })
  ```

  ```typescript v3 theme={null}
  const search = sdk.search({ query: 'shoes' })
  await search.get()
  ```
</CodeGroup>

Search still auto-runs prepare and retries on `425`. `prepare()` is exposed for pre-warming without fetching results:

```typescript theme={null}
const search = sdk.search({ query: 'shoes' })
await search.prepare() // optional
await search.get()
```

### autocomplete

`suggest` was renamed back to `autocomplete`. Debounce is no longer built in — use `createDebounce`:

<CodeGroup>
  ```typescript v2 theme={null}
  const suggest = client.suggest({ debounce: 300 })
  suggest.execute('shoes')
  ```

  ```typescript v3 theme={null}
  import { createDebounce } from '@commerce-blocks/sdk'

  const ac = sdk.autocomplete()
  const debounced = createDebounce((q: string) => ac.get({ query: q }), 300)
  debounced.call('shoes')
  ```
</CodeGroup>

For an easier migration path, `sdk.suggest({ debounce })` remains as a deprecated wrapper around `autocomplete()` with built-in debounce.

### blocks

<CodeGroup>
  ```typescript v2 theme={null}
  const blocks = client.blocks({ blockId: '...', anchor: '123' })
  await blocks.execute({ discounts: [...] })
  ```

  ```typescript v3 theme={null}
  const blocks = sdk.blocks({ blockId: '...', anchorId: '123' })
  await blocks.get()
  ```
</CodeGroup>

| v2       | v3         |
| -------- | ---------- |
| `anchor` | `anchorId` |

### imageSearch

<CodeGroup>
  ```typescript v2 theme={null}
  const controller = client.searchByImage({ imageId: 'img-123' })
  ```

  ```typescript v3 theme={null}
  const controller = sdk.imageSearch({ imageId: 'img-123' })
  await controller.get()
  ```
</CodeGroup>

`searchByImage()` remains as a deprecated alias.

### uploadImage

Same controller interface as v2 (`state`, `subscribe`, `get`, `dispose`). Each `get()` still uploads — this controller is intentionally uncached.

```typescript theme={null}
const upload = sdk.uploadImage({ image: file })
const { data } = await upload.get()
// data.imageId — pass to sdk.imageSearch()
```

### searchContent

<CodeGroup>
  ```typescript v2 theme={null}
  const content = client.searchContent()
  await content.execute({ query: 'shipping', contentType: 'article' })
  ```

  ```typescript v3 theme={null}
  const content = sdk.searchContent({ query: 'shipping', contentType: 'article' })
  await content.get()
  ```
</CodeGroup>

Articles keep their v2 shape and pick up `typename`, `contentType`, `contentId`, plus `totalPages` on the response.

### similarProducts (new)

Find products similar to a given product by ID:

```typescript theme={null}
const similar = sdk.similarProducts({ searchProductId: 12345 })
const { data } = await similar.get()
// data.products, data.totalResults
```

### facets (new)

Facets are no longer declared at init. Fetch them per query:

```typescript theme={null}
const facets = sdk.facets({
  collectionHandle: 'all',
  facets: ['options.color', 'options.size'],
  retrieveFacetCount: true,
  includeFacetRanges: true,
})

const { data } = await facets.get()
// data.facets, data.facetRanges
```

### sortOrders (new)

Sort orders are fetched at runtime:

```typescript theme={null}
const sorts = sdk.sortOrders()
const { data } = await sorts.get()
// data.sortOrders: [{ code, nickname }, ...]
```

### searchFeedback (new direct call)

Send search-quality feedback. Not a controller — returns a plain promise:

```typescript theme={null}
await sdk.searchFeedback({
  searchId: 'search-abc123',
  rating: 'positive',
  textFeedback: 'Great results',
  productFeedback: [{ productId: 42, rating: 'positive' }],
})
```

### trackingEvent (new direct call)

Send analytics events via `sendBeacon` with a fetch fallback:

```typescript theme={null}
sdk.trackingEvent({
  eventType: 'product_click',
  sessionId: 'sess-123',
  attributionToken: 'attr-abc',
  productId: 42,
  position: 3,
})

// Batch
sdk.trackingEvent([event1, event2])
```

Event types: `collection_view`, `product_click`, `product_view`, `product_impression`, `product_hover`, `product_touch`, `add_to_cart`, `search`, `autocomplete`, `block_view`.

## Filter DSL

Operator renames and additions:

| v2            | v3                 | Notes   |
| ------------- | ------------------ | ------- |
| `notEq()`     | `neq()`            | Renamed |
| `exists()`    | `notNull()`        | Renamed |
| `notExists()` | `isNull()`         | Renamed |
| —             | `between()`        | New     |
| —             | `notBetween()`     | New     |
| —             | `contains()`       | New     |
| —             | `doesNotContain()` | New     |
| —             | `beginsWith()`     | New     |
| —             | `endsWith()`       | New     |

```diff theme={null}
- import { eq, notEq, exists, and, filter } from '@commerce-blocks/sdk'
- filter(and(eq('vendor', 'Nike'), exists('category')))
+ import { eq, neq, notNull, and, filter } from '@commerce-blocks/sdk'
+ filter(and(eq('vendor', 'Nike'), notNull('category')))
```

The underlying `FilterOperator` enum values also changed: `not_eq` → `neq`, `not_in` → `notIn`, `exists` → `notNull`, `not_exists` → `null`. Core operators (`eq`, `inValues`, `notIn`, `gt`, `gte`, `lt`, `lte`, `and`, `or`, `filter`) are unchanged.

Filter aliases (`filterAliases`) are removed — build filter expressions with the DSL directly.

## Error handling

The typed error hierarchy is gone. Every API returns `{ data, error }` with a plain `Error`:

<CodeGroup>
  ```typescript v2 theme={null}
  import { isClientError, isRetryable, type ClientError } from '@commerce-blocks/sdk'

  if (error?._tag === 'NetworkError') { /* ... */ }
  if (isRetryable(error)) { /* ... */ }
  ```

  ```typescript v3 theme={null}
  if (error instanceof Error) {
    console.log(error.message) // e.g. "HTTP 401: Unauthorized"
  }
  ```
</CodeGroup>

Removed: `ClientError`, `NetworkError`, `ApiError`, `ValidationError`, `ConfigError`, `isClientError`, `isRetryable`. Internal functions no longer throw — `{ data, error }` all the way down.

## Storage

`StorageAdapter` was replaced with `StorageBackend`, which matches the standard `Storage` interface. Pass `localStorage` directly:

<CodeGroup>
  ```typescript v2 theme={null}
  import { localStorageAdapter } from '@commerce-blocks/sdk'

  createClient({ storage: localStorageAdapter('my-key') })
  ```

  ```typescript v3 theme={null}
  createSDK({
    storefrontAccessToken: '...',
    trackingToken: '...',
    storage: localStorage,
  })
  ```
</CodeGroup>

```typescript theme={null}
type StorageBackend = Pick<Storage, 'getItem' | 'setItem' | 'removeItem' | 'clear'>
```

For Node.js or SSR, implement `StorageBackend` directly. `localStorageAdapter()` and `fileStorage()` are removed.

## Cache

The public `client.cache` API is gone. Configure the in-memory LRU via `SdkConfig`:

<CodeGroup>
  ```typescript v2 theme={null}
  client.cache.get(key)
  client.cache.set(key, result)
  client.cache.invalidate(pattern)
  client.cache.clear()
  ```

  ```typescript v3 theme={null}
  createSDK({
    storefrontAccessToken: '...',
    trackingToken: '...',
    cache: { ttl: 300_000, maxSize: 100 },
  })
  ```
</CodeGroup>

`uploadImage` bypasses the cache — each `get()` uploads. `searchFeedback` and `trackingEvent` are not cached.

## New in v3

| Feature                     | Description                                                |
| --------------------------- | ---------------------------------------------------------- |
| `sdk.similarProducts()`     | Find similar products by product ID                        |
| `sdk.facets()`              | Fetch facets at runtime (replaces the config array)        |
| `sdk.sortOrders()`          | Fetch sort orders at runtime (replaces the config array)   |
| `sdk.searchFeedback()`      | Send search-quality feedback                               |
| `sdk.trackingEvent()`       | Analytics via `sendBeacon` with a fetch fallback           |
| `sdk.suggest()`             | Deprecated `autocomplete()` wrapper with built-in debounce |
| `subscribe(controller, cb)` | Standalone subscribe utility                               |
| `controller.state` getter   | Direct state access (no `.value`)                          |
| `createDebounce(fn, ms)`    | Standalone debounce with `call`/`cancel`/`flush`           |
| `resetSDK()`                | Clear the singleton instance                               |
| `trackingToken` config      | Separate analytics token                                   |
