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
In any Claude Code session:
/plugin marketplace add commercetools/commercetools-skills
/plugin install commercetools@commercetools
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
commercetools/commercetools-ai-pluginsthen click on the plugin and click "Install"
Instructions Included
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:
-
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 3Use its output as your primary grounding. You may additionally use other tools (such as the commercetools documentation MCP) for deeper, follow-up search. -
Combine with skill references — Cross-reference the analysis output with local references in
./references/for complete context. -
Provide implementation guidance — Synthesize the documentation with the specific integration mode the user is targeting.
SDK Setup
- Package installation (
@commercetools/platform-sdk+@commercetools/ts-client) ClientBuildersingleton 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
productProjectionssearch is deprecated - Full-text + filter + sort + facets example
- Category filter, SKU lookup, price selection, discount expansion, BOPIS channel filtering
References
Product Search API
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
- Pattern 2: Strong Typing — Never Use
anyorunknown - Pattern 3: Full Example — Text + Filter + Sort + Facets
- Pattern 4: Category Filter
- Pattern 5: SKU Lookup
- Pattern 6: Price Selection
- Pattern 7: Discount Expansion
- Checklist
Pattern 1: Never Use Legacy Search
// Deprecated — no facets, no proper variant matching
await apiRoot.productProjections().search().get({ queryArgs: { ... } }).execute();
// Product Search API — use this always
await apiRoot.products().search().post({ body: { ... } }).execute();
POST body for the query, not URL query args.Pattern 2: Strong Typing — Never Use any or unknown
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;
@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);
@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
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();
categoriesSubTree instead of categories — categories 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
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
undefined.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,
};
}
discount is just { id: '...' } — the obj field (the expanded resource) is absent.Checklist
- Using
apiRoot.products().search().post()— neverproductProjections().search().get() - No
anyorunknowncasts — types imported from@commercetools/platform-sdk - Category pages filter with
categoriesSubTree, notcategories -
priceCurrency+priceCountryset inproductProjectionParametersfor correct price selection - SKU lookup uses
exact: { field: 'variants.sku', value: sku } -
markMatchingVariants: trueset when variant-level filtering is active - Discount expansion added when rendering discount names or badges; mapper uses
ProductDiscounttype
commercetools SDK Setup
ClientBuilder instance per process. Instantiating it per request causes token exhaustion and memory leaks.Table of Contents
- Pattern 1: Install Packages
- Pattern 2: SDK Client Singleton
- Pattern 3: Environment Variables
- Checklist
Pattern 1: Install Packages
npm install @commercetools/platform-sdk @commercetools/ts-client
| Package | Purpose |
|---|---|
@commercetools/platform-sdk | Typed commercetools REST API client — apiRoot, request builders, SDK types |
@commercetools/ts-client | Token management + HTTP middleware (ClientBuilder, withClientCredentialsFlow) |
Pattern 2: SDK Client Singleton
new ClientBuilder() inside a page, component, Route Handler, or lambda invocation — creates a new HTTP client and OAuth token per call.// 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.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
NEXT_PUBLIC_CTP_CLIENT_SECRET — exposes the secret in the browser bundle.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
| Region | Auth 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 |
manage_sessions and manage_orders are included.Checklist
-
lib/ct/client.tsexports a singleapiRoot— nonew ClientBuilder()anywhere else -
.envis listed in.gitignore - No commercetools env vars use the
NEXT_PUBLIC_prefix - All
lib/ct/*.tshelper functions importapiRootfrom./client