Best practices

Ask about this Page
Copy for AI
View as Markdown

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, and country when you mint a session. Queries without presentment fail with SESSION_MISSING_PRESENTMENT.
  • Refresh, don't re-mint: use the refresh grant to renew tokens without re-authenticating. Use DELETE /session to 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 fetch and 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 @requires to pull in necessary Commerce Integration Layer data.
  • Read Project data through the Commerce Integration Layer: avoid instantiating a commercetools client in a resolver. Use @requires to receive the data you need on the resolver parent; 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.config and store secrets as secret configuration 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

ResourceLimit
Extension bundle (uploaded code)4 MiB

Spotting this in logs

Look for these indicators in your logs:

  • High http_client_request_duration on EXTENSION HTTP REQUEST log 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 null on failure so the rest of the response survives. Throw a GraphQLError only 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: validate composes your extension against your Project and flags collisions and breaking changes. Treat a breaking change as deliberate and coordinated. Override the check with --force only when consumers are ready.
  • Record a revision: push records 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 -dirty revision.
  • Remember one bundle per Project: a push replaces the previous bundle. To ship several patterns at once, combine their typeDefs, resolvers, and apiExtensions in a single bundle.

Migrate from an existing BFF

For migration planning and supported patterns, see the dedicated migration guide.