Recommendations and best practices for building storefronts and extensions.
For runtime limits, rollout states, and live-version checks, see Operations and performance.
Sessions
- Keep the commercetools token server-side: the Commerce Integration Layer manages the trust boundary. Store only the session token in the browser; if you store it in an HttpOnly cookie, use a same-origin proxy so the cookie stays first-party (many browsers block third-party cookies).
- Skip the session cookie proxy for mobile apps: store the session token securely (such as Keychain or Android Keystore) and call identity and GraphQL hosts directly. See Sessions across touchpoints.
- Always set presentment: pass
locale,currency, andcountrywhen you mint a session. Queries without presentment fail withSESSION_MISSING_PRESENTMENT. - Refresh, don't re-mint: use the
refreshgrant to renew tokens without re-authenticating. UseDELETE /sessionto sign out immediately. - Upgrade in place: send the anonymous session as the bearer when signing a shopper in, so their Cart merges into the customer session.
Querying the API
- Request only what you need: the merged schema is broad; select the fields your view renders. This keeps responses small and lets the router plan the most efficient query.
- Explore the schema from the SDL: introspection is unavailable on the public endpoint. Fetch your Project schema with
commercetools integration-layer schema fetchand use it in your editor for autocompletion and validation. - Handle nullable fields: fields backed by external services or entitlement-gated data can return
null. Render around missing values rather than assuming they are always present.
Designing extensions
- Add fields; avoid replacing them: add new fields instead of redefining existing ones. Use
@requiresto pull in necessary Commerce Integration Layer data. - Read Project data through the Commerce Integration Layer: avoid instantiating a commercetools client in a resolver. Use
@requiresto receive the data you need on the resolverparent; the read stays inside the trust boundary and is batched server-side. - Return entity stubs: when a field returns an entity such as a
Product, return just its{ id }and let the router resolve the rest. Your resolver decides which entities to return; the Commerce Integration Layer stays the source of truth for their contents. - Keep configuration out of the bundle: read per-project settings from
ctx.configand store secrets assecretconfiguration entries. The same bundle then runs unchanged across Projects. - Keep extensions stateless: each extension runs per request in a restricted sandbox with no shared store and no memory that survives the request. Keep state such as caches, locks, and idempotency keys in an external service, not in the extension.
Keep payloads small
- As a guideline, keep data fetched or processed under 5 MB per call. Extensions run synchronously during shopper requests, and resolvers get a 30-second execution timeout by default. Processing large payloads increases processing time on every request, not just the first. Example: Fetching a full vendor catalog and filtering it in memory on every request delays page load for every shopper. Use our filtering APIs or cache the result instead.
This advice applies to data fetched or processed during execution, not your uploaded bundle size (governed by separate limits).
Hard limits
| Resource | Limit |
|---|---|
| Extension bundle (uploaded code) | 4 MiB |
Spotting this in logs
Look for these indicators in your logs:
- High
http_client_request_durationonEXTENSION HTTP REQUESTlog lines (slow outbound calls).
See Outbound HTTP calls for the full field reference and recommended alerts.
http_response_body_size on EXTENSION HTTP REQUEST uses the response's Content-Length header. It measures wire size (which may be compressed), not decoded in-memory size.Keep the right work in the layer
The Commerce Integration Layer is a shopper-facing BFF, not a general-purpose backend. Route work that is not shopper-facing outside the layer:
- Scheduled or event-driven work: data reconciliation, feed exports, and other background jobs belong in Connect or your own background services. An extension runs per request and cannot run background jobs.
- In-store and point-of-sale flows: use InStore or dedicated client gateways for point-of-sale orchestration.
- Machine-to-machine integrations: server-to-server calls and inbound webhooks that no storefront reads belong in dedicated backend services or Connect applications, not in an extension.
For the full scope of the layer, see the overview.
Resilience and degradation
- Degrade gracefully: for anything that can fail transiently, such as an external API call, make the field nullable and return
nullon failure so the rest of the response survives. Throw aGraphQLErroronly when a field should fail on purpose. - Use least-privilege credentials: give an extension the narrowest credential that works (for example, a search-only key for a search resolver, never an administrator key).
- Fail write operations explicitly: a Managed API Extension handler that throws blocks the write rather than approving it. Return an explicit approval or a validation error, and keep the handler well within commercetools' extension timeout.
Set up alerts
commercetools monitors the platform, but not the code and dependencies of your extensions. Set up alerts in your Application Performance Monitoring (APM) tool for Platform Insights signals. For outbound HTTP logs and runtime
api_extension.timeout events, poll the Deployment Logs API or CLI and feed the results into your own alerting.For the signals, fields, and recommended alerts, see Monitoring and alerting.
Publishing and provenance
- Validate before you publish:
validatecomposes your extension against your Project and flags collisions and breaking changes. Treat a breaking change as deliberate and coordinated. Override the check with--forceonly when consumers are ready. - Record a revision:
pushrecords your version-control revision with the bundle, so you can tell which of your commits a Project is running. Push from a clean working tree to avoid a-dirtyrevision. - Remember one bundle per Project: a push replaces the previous bundle. To ship several patterns at once, combine their
typeDefs,resolvers, andapiExtensionsin a single bundle.
Migrate from an existing BFF
For migration planning and supported patterns, see the dedicated migration guide.