commercetools TypeScript SDK

Description

Foundational patterns for connecting to the commercetools API from TypeScript. These patterns are project-type agnostic — they apply whether you are building a Next.js storefront, a serverless function, or a CLI script.

Installation

Recommended: install the full commercetools plugin. It includes this Skill, every other commercetools Skill, our pre-tuned Subagents, and the commercetools Knowledge MCP — which gives AI live access to the commercetools docs, GraphQL/OpenAPI schemas, and query validation. You only install once; every Skill on this site becomes available in every session.
Install the plugin

In any Claude Code session:

/plugin marketplace add commercetools/commercetools-skills
/plugin install commercetools@commercetools
Reload plugins

If you've updated the plugin or installed it in another window and need the current session to pick up the latest version:

/reload-plugins
Claude Desktop
Customize -> Personal plugins -> Create plugin -> Add marketplace -> add commercetools/commercetools-ai-plugins

then click on the plugin and click "Install"

Instructions Included

SKILL.md

commercetools TypeScript SDK

Foundational patterns for connecting to the commercetools API from TypeScript. These patterns are project-type agnostic — they apply whether you are building a Next.js storefront, a serverless function, or a CLI script.

Workflow

When this skill is invoked, always follow these steps:

  1. Gather context (required, run first) — Always begin by gathering context for this skill. This is the mandatory grounding step: it gathers the latest verified documentation as context for you (the agent). Do not skip it, and do not replace it with another tool (such as an MCP documentation-search tool) — run this command:
    node scripts/gather-context.mjs \
      --query "<extract key terms from user's question>" \
      --client-name "<current-client>" \
      --model "<current-model>" \
      --skill-name "commercetools-platform" \
      --limit 3
    
    Use its output as your primary grounding. You may additionally use other tools (such as the commercetools documentation MCP) for deeper, follow-up search.
  2. Combine with skill references — Cross-reference the analysis output with local references in ./references/ for complete context.
  3. Provide implementation guidance — Synthesize the documentation with the specific integration mode the user is targeting.

SDK Setup

See sdk-setup.md for:
  • Package installation (@commercetools/platform-sdk + @commercetools/ts-client)
  • ClientBuilder singleton with Client Credentials flow
  • Required environment variables and auth URLs by region
  • Required API client scopes

Product Search API

  • Official docs and why the legacy productProjections search is deprecated
  • Full-text + filter + sort + facets example
  • Category filter, SKU lookup, price selection, discount expansion, BOPIS channel filtering

References

product-search.md

Product Search API

Impact: HIGH — The legacy productProjections search endpoint is deprecated and lacks facets and proper variant matching. Always use the Product Search API.

Table of Contents


Pattern 1: Never Use Legacy Search

INCORRECT:
// Deprecated — no facets, no proper variant matching
await apiRoot.productProjections().search().get({ queryArgs: { ... } }).execute();
CORRECT:
// Product Search API — use this always
await apiRoot.products().search().post({ body: { ... } }).execute();
The Product Search API uses a POST body for the query, not URL query args.

Pattern 2: Strong Typing — Never Use any or unknown

INCORRECT: casting to any or unknown to work around missing types.
// BAD — loses all type safety
const name = (result as any).productProjection?.name?.['en-US'];
const discount = (ctPrice.discounted?.discount?.obj as any)?.name;
CORRECT: import and use the SDK types from @commercetools/platform-sdk directly.
import type {
  ProductSearchResult,
  ProductProjection,
  ProductVariant,
  Price as CtPrice,
  ProductDiscount,
  LocalizedString,
} from '@commercetools/platform-sdk';

// Result typing
const result: ProductSearchResult = body.results[0];
const projection: ProductProjection | undefined = result.productProjection;

// Expanded discount reference — obj is typed as ProductDiscount | undefined
const discountObj = ctPrice.discounted?.discount?.obj as ProductDiscount | undefined;
const discountName = getLocalizedString(discountObj?.name as LocalizedString | undefined, locale);
The @commercetools/platform-sdk exports types for every resource, reference, and expanded object in the API. Search the package exports before reaching for any.

Pattern 3: Full Example — Text + Filter + Sort + Facets

import { apiRoot } from './client';
import type { ProductSearchRequest } from '@commercetools/platform-sdk';

const searchRequest: ProductSearchRequest = {
  query: {
    and: [
      {
        fullText: {
          field: 'name',
          language: 'en-US', // Always use BCP-47
          value: 'cotton shirt',
        },
      },
      {
        filter: [
          {
            exact: {
              field: 'variants.attributes.color',
              fieldType: 'ltext',
              language: 'en-US', // Always use BCP-47
              value: 'Blue',
            },
          },
        ],
      },
    ],
  },
  sort: [
    { field: 'name', language: 'en-US', order: 'asc' },
  ],
  facets: [
    {
      distinct: {
        name: 'categories',
        field: 'categories.id',
      },
    },
    {
      distinct: {
        name: 'sizes',
        field: 'variants.attributes.size',
        fieldType: 'enum',
      },
    },
    {
      ranges: {
        name: 'price-ranges',
        field: 'variants.prices.centAmount',
        ranges: [
          { from: 0,    to: 2000 },
          { from: 2000, to: 5000 },
          { from: 5000 },
        ],
      },
    },
  ],
  markMatchingVariants: true,
  limit: 20,
  offset: 0,
};

const { body } = await apiRoot.products().search().post({ body: searchRequest }).execute();
// body.results[].productProjection — mapped by lib/mappers/product.ts
// body.facets — array of ProductSearchFacetResult - ask commercetools-developer-tips about ProductSearchFacetResult
// body.total, body.offset, body.limit — for pagination

Pattern 4: Category Filter

Filter to products in a category and all its subcategories using categoriesSubTree:
const { body } = await apiRoot.products().search().post({
  body: {
    query: {
      exact: {
        field: 'categoriesSubTree',
        value: categoryId,   // commercetools category ID
      },
    },
    productProjectionParameters: {
      priceCurrency: 'USD',
      priceCountry:  'US',
    },
    limit: 24,
    offset: 0,
  },
}).execute();
Use categoriesSubTree instead of categoriescategories matches only the exact category, not descendants.

Pattern 5: SKU Lookup

Fetch a single product by exact SKU match:

import type { ProductSearchRequest, ProductProjection } from '@commercetools/platform-sdk';

const { body } = await apiRoot.products().search().post({
  body: {
    query: {
      exact: {
        field: 'variants.sku',
        value: sku,
      },
    } as ProductSearchRequest['query'],
    productProjectionParameters: {
      priceCurrency: currency,
      priceCountry:  country,
      localeProjection: [locale],
    },
    limit: 1,
  },
}).execute();

const projection: ProductProjection | undefined = body.results[0]?.productProjection;
// projection is undefined when the SKU doesn't exist — call notFound() in the page

Pattern 6: Price Selection

Pass priceCurrency + priceCountry in productProjectionParameters. commercetools selects the matching price tier automatically — each variant arrives with .price already resolved to the correct currency/country combination.
productProjectionParameters: {
  priceCurrency: 'EUR',
  priceCountry:  'DE',
}
// variant.price is now the EUR price for Germany — no client-side filtering needed

Pattern 7: Discount Expansion

INCORRECT: not expanding discount references — the discount name is undefined.
CORRECT: expand masterVariant and variants discount refs, and use SDK types in the mapper:
// In the search call
productProjectionParameters: {
  priceCurrency: currency,
  priceCountry:  country,
  expand: [
    'masterVariant.price.discounted.discount',
    'variants[*].price.discounted.discount',
  ],
},
// In the price mapper — use ProductDiscount, not any
import type { Price as CtPrice, ProductDiscount, LocalizedString } from '@commercetools/platform-sdk';

function mapPrice(ctPrice: CtPrice): Price {
  const discountObj = ctPrice.discounted?.discount?.obj as ProductDiscount | undefined;
  return {
    value: mapMoney(ctPrice.value),
    discounted: ctPrice.discounted
      ? {
          value:        mapMoney(ctPrice.discounted.value),
          discountName: getLocalizedString(discountObj?.name as LocalizedString | undefined, locale),
        }
      : undefined,
  };
}
Without expansion, discount is just { id: '...' } — the obj field (the expanded resource) is absent.

Checklist

  • Using apiRoot.products().search().post() — never productProjections().search().get()
  • No any or unknown casts — types imported from @commercetools/platform-sdk
  • Category pages filter with categoriesSubTree, not categories
  • priceCurrency + priceCountry set in productProjectionParameters for correct price selection
  • SKU lookup uses exact: { field: 'variants.sku', value: sku }
  • markMatchingVariants: true set when variant-level filtering is active
  • Discount expansion added when rendering discount names or badges; mapper uses ProductDiscount type
sdk-setup.md

commercetools SDK Setup

Impact: CRITICAL — One ClientBuilder instance per process. Instantiating it per request causes token exhaustion and memory leaks.

Table of Contents


Pattern 1: Install Packages

npm install @commercetools/platform-sdk @commercetools/ts-client
PackagePurpose
@commercetools/platform-sdkTyped commercetools REST API client — apiRoot, request builders, SDK types
@commercetools/ts-clientToken management + HTTP middleware (ClientBuilder, withClientCredentialsFlow)

Pattern 2: SDK Client Singleton

INCORRECT: new ClientBuilder() inside a page, component, Route Handler, or lambda invocation — creates a new HTTP client and OAuth token per call.
CORRECT — one module-level singleton, imported everywhere:
// lib/ct/client.ts
import { createApiBuilderFromCtpClient } from '@commercetools/platform-sdk';
import { ClientBuilder } from '@commercetools/ts-client';

const projectKey = process.env.CTP_PROJECT_KEY!;
const authUrl    = process.env.CTP_AUTH_URL!;
const apiUrl     = process.env.CTP_API_URL!;

function buildClient() {
  return new ClientBuilder()
    .withProjectKey(projectKey)
    .withClientCredentialsFlow({
      host: authUrl,
      projectKey,
      credentials: {
        clientId:     process.env.CTP_CLIENT_ID!,
        clientSecret: process.env.CTP_CLIENT_SECRET!,
      },
      scopes: [process.env.CTP_SCOPES!],
    })
    .withHttpMiddleware({ host: apiUrl })
    .build();
}

export const apiRoot = createApiBuilderFromCtpClient(buildClient())
  .withProjectKey({ projectKey });

export { projectKey, apiUrl, authUrl };
withClientCredentialsFlow handles OAuth 2.0 token fetching and auto-refresh transparently — you never call the auth endpoint directly.
Every helper function imports apiRoot from this file:
import { apiRoot } from './client';

export async function getSomething(id: string) {
  const { body } = await apiRoot.things().withId({ ID: id }).get().execute();
  return body;
}

Pattern 3: Environment Variables

INCORRECT: NEXT_PUBLIC_CTP_CLIENT_SECRET — exposes the secret in the browser bundle.
CORRECT — all commercetools variables are server-only (no NEXT_PUBLIC_ prefix):
# .env  (add to .gitignore — never commit)
CTP_PROJECT_KEY=your-project-key
CTP_AUTH_URL=https://auth.us-central1.gcp.commercetools.com
CTP_API_URL=https://api.us-central1.gcp.commercetools.com
CTP_CLIENT_ID=your-client-id
CTP_CLIENT_SECRET=your-client-secret
# B2C example
CTP_SCOPES=manage_order_edits:your-project-key view_sessions:your-project-key view_product_selections:your-project-key view_shipping_methods:your-project-key manage_shopping_lists:your-project-key view_discount_codes:your-project-key manage_customers:your-project-key view_types:your-project-key manage_sessions:your-project-key manage_orders:your-project-key view_standalone_prices:your-project-key view_tax_categories:your-project-key view_published_products:your-project-key view_cart_discounts:your-project-key create_anonymous_token:your-project-key view_project_settings:your-project-key view_products:your-project-key view_categories:your-project-key
Auth URL by region:
RegionAuth URL
Americas (GCP)https://auth.us-central1.gcp.commercetools.com
Europe (GCP)https://auth.europe-west1.gcp.commercetools.com
Australia (GCP)https://auth.australia-southeast1.gcp.commercetools.com
Required API client scopes (Merchant Center → Settings → Developer Settings): Use Frontend B2C template (or Frontend B2B), then make sure manage_sessions and manage_orders are included.

Checklist

  • lib/ct/client.ts exports a single apiRoot — no new ClientBuilder() anywhere else
  • .env is listed in .gitignore
  • No commercetools env vars use the NEXT_PUBLIC_ prefix
  • All lib/ct/*.ts helper functions import apiRoot from ./client