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-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
See sdk-setup.md for:
- 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
See product-search.md for:
- 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
Official docs: https://docs.commercetools.com/api/projects/product-search
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
- 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
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 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
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()— 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
Impact: CRITICAL — One
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
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:
| 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 |
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.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