Customization and extensibility
The framework provides sensible defaults for every commerce domain. When your project needs different behavior, you can override individual resolvers, extend schemas with your own modules, or register custom mappers for CMS content types.
Use modules as-is
Section titled “Use modules as-is”The simplest approach: compose framework modules without modification. This is the right choice when the default resolvers and schemas match your requirements.
import { QuoteModule } from "@evolve-framework/commercetools";import { CompositeModule } from "@evolve-framework/core";
export const createModule = (): CompositeModule => { return new CompositeModule([new QuoteModule()]);};Override individual resolvers
Section titled “Override individual resolvers”When you need to change how a specific operation works, subclass the framework module and replace the resolver entry:
import { QuoteModule } from "@evolve-framework/commercetools";import { customQuoteRequestCreate } from "./resolvers/quote-request-create.ts";
export class ProjectQuoteModule extends QuoteModule { constructor() { super(); this.resolvers = { ...this.resolvers, Mutation: { ...this.resolvers.Mutation, quoteRequestCreate: customQuoteRequestCreate, }, }; }}
// Compose with your override instead of the default:const module = new CompositeModule([new ProjectQuoteModule()]);This replaces only the quoteRequestCreate mutation. All other quote
resolvers keep their default behavior, and the schema is unchanged.
Alternatively, compose a small module after the framework module:
CompositeModule merges resolvers per GraphQL type name, and the later
module in the array wins for each field. Note that a module only
contributes when it has type definitions — an override module must carry
typedefs for the fields it re-resolves, and those definitions must match
the framework’s exactly or the merge throws at startup. For plain resolver
swaps, subclassing is usually the cleaner option.
Add project-specific modules
Section titled “Add project-specific modules”For functionality that does not exist in the framework, create your own
module by extending AbstractModule:
import { AbstractModule } from "@evolve-framework/core";import { gql } from "graphql-tag";import { loyaltyResolver } from "./resolvers/loyalty.ts";
export class LoyaltyModule extends AbstractModule { typedefs = gql` type LoyaltyAccount { points: Int! tier: String! } extend type Customer { loyalty: LoyaltyAccount } `;
resolvers = { Customer: { loyalty: loyaltyResolver, }, };}Compose it alongside framework modules:
const module = new CompositeModule([ new CustomerModule({ config: customerConfig }), new LoyaltyModule(),]);CompositeModule merges all type definitions and resolvers, so your
extend type Customer works seamlessly with the framework’s customer
schema. If your module needs an SDK client or other side-effecty setup,
put it in init() — constructors must stay cheap so codegen tooling can
instantiate the module without runtime config.
A custom module can also expose HTTP routes by implementing
getHttpConfig(); the composite mounts them on the service’s Fastify
instance alongside the module-provided routes such as CMS webhooks.
Module configuration
Section titled “Module configuration”Modules take typed options through their constructor — each declares
exactly what it needs, from a clientFactory for most commercetools
modules to a full config object:
new CustomerModule({ config: { CTP_PROJECT_KEY: config.CTP_PROJECT_KEY, CTP_AUTH_URL: config.CTP_AUTH_URL, CTP_USER_CLIENT_ID: config.CTP_USER_CLIENT_ID, CTP_USER_CLIENT_SECRET: config.CTP_USER_CLIENT_SECRET, },});Because the options are constructor arguments, misconfiguration is a type error at the composition site rather than a runtime failure in a resolver.
Custom CMS content types: the mapper registry
Section titled “Custom CMS content types: the mapper registry”The CMS packages map CMS entries onto the shared content schema (blocks,
column entries, links). The mapping is driven by a MapperRegistry keyed
by content type id, and this registry is the primary customization point
for CMS services: you rarely touch resolvers, you register mappers.
ContentfulModule builds the registry during init(): it installs the
default mappers for the built-in content types, then invokes your optional
mappers callback. Registering a mapper for an already-known content type
overrides the default without throwing.
import type { ColumnEntryMapper } from "@evolve-framework/contentful";import { ContentfulModule } from "@evolve-framework/contentful";import { NoopPublisher } from "@evolve-framework/core/messaging";import type { Entry } from "contentful";import type { TypePageTitleSkeleton } from "#src/cftypes/index.ts";
const mapPageTitle: ColumnEntryMapper = (context, entry) => { const data = entry as Entry< TypePageTitleSkeleton, "WITHOUT_UNRESOLVABLE_LINKS", string >; return { __typename: "PageTitle", id: data.sys.id, title: data.fields.title.toUpperCase(), };};
new ContentfulModule({ config: { spaceId: config.CONTENTFUL_SPACE_ID, environment: config.CONTENTFUL_ENVIRONMENT, accessToken: config.CONTENTFUL_ACCESS_TOKEN, previewToken: config.CONTENTFUL_PREVIEW_TOKEN, managementToken: config.CONTENTFUL_MANAGEMENT_API_ACCESS_TOKEN, }, messagePublisher: new NoopPublisher(), mappers: (registry) => { registry.registerColumnEntry("pageTitle", mapPageTitle); },});The registry has three registration methods, one per mapper kind:
registerBlock(contentTypeId, mapper): top-level page blocks (defaults includehero,productRow,uspList,contentTeaserRow)registerColumnEntry(contentTypeId, mapper): entries inside column blocks (defaults includerichText,faq,image,pageTitle)registerLink(contentTypeId, mapper): link-like entries (defaults includepageContent,pageCatalog,resourceLink)
Entries with no registered mapper are logged and skipped, so an unknown content type degrades to a missing block rather than an error.
For an entirely new content type, register a mapper and extend the
content schema: compose a small AbstractModule alongside
ContentfulModule that adds your GraphQL type (for example a new Block
union member), then return that __typename from your mapper.
Testing approach
Section titled “Testing approach”The framework ships with its own test suite covering default behavior. Your project tests should focus on what you own:
- Custom resolvers and mappers: test any resolver or mapper you override or add
- Module composition: verify that your module combination produces the
expected schema (composition errors such as type conflicts surface at
init()) - REST endpoints, event handlers, and jobs: test project-specific business logic
Test utilities
Section titled “Test utilities”DomainService itself is test-friendly: after init() (or
createDomainService()), use service.fastify.inject(...) to make
requests without binding a port.
The @evolve-framework/commercetools/testing export provides
createTestServer() and createTestExecutor() for GraphQL integration
tests, plus ctMock and ctMockClient for mocking commercetools API
calls. The @evolve-framework/commercetools/factories export provides
fishery-based factories for generating realistic test data (cart drafts,
customer drafts, categories, and more) instead of writing mock data by
hand.

