core API
Everything the package exports, by the path you import it from. Open an export for its signature and options.
@evolve-framework/core
The application, the module system (Module, AbstractModule, CompositeModule), and health checks.
Classes
AbstractModuleThe base class of every module.
import { AbstractModule } from "@evolve-framework/core";new AbstractModule()| Method | Description |
|---|---|
getGraphQLConfig(): GraphQLConfig | undefined | The type definitions and resolvers this module adds to the GraphQL schema, or undefined when it serves no GraphQL. |
getGraphQLSchema(): string | undefined | The type definitions of getGraphQLConfig() printed as SDL, which codegen reads without running the service. |
getHttpConfig(): HttpConfig | undefined | The routes and route-scoped Fastify plugins this module registers on the HTTP server, or undefined when it has none. |
getInternalEventConfig(): InternalEventConfig | undefined | The internal events this module handles, if any. |
init(): void | Promise<void> | One-shot initialization hook for side-effecty setup that must not run at construction time — typically building SDK clients or registering module-scoped singletons. |
close(): void | Promise<void> | Optional cleanup hook called by Application.stop() after the HTTP server has closed. |
ApplicationApplication composes a Module with its HTTP/GraphQL surface and runs it.
import { Application } from "@evolve-framework/core";Lifecycle:
new Application(options)— cheap, no side effects.await service.init()— runsmodule.init()then builds the GraphQL and Fastify instances. Idempotent.await service.start()— runsonStart, binds the port and installs signal handlers. A signal-driven shutdown runsstop(), thenonShutdown.
Tests can use service.fastify.inject(...) after init() without calling
start(). Use createApplication for the common one-liner that
does steps 1 and 2 together.
The GraphQL user-context type lives on ApplicationOptions<TUserContext>
(where it couples graphql.context to graphql.plugins), not on the
service itself — the service exposes nothing typed by it.
new Application(options: ApplicationOptions<any>)| Option | Type | Description |
|---|---|---|
name | string | The name of the domain service, used for identification and logging. |
module | Module<any> | The module that encapsulates the domain's business logic, GraphQL schema, and resolvers. This is a required property as the domain service relies on the module to define its functionality. |
http | HTTPServerOptions | HTTP server configuration. Required to start the HTTP server that handles both GraphQL and REST requests. |
onStart? | (() => void | Promise<void>) | Sets up what the modules share, such as the cache connection, when
|
onShutdown? | (() => void | Promise<void>) | Releases what a finished |
graphql? | GraphQLOptions<any> | Optional GraphQL server configuration. This is only used if the module provides a GraphQL configuration. If the module does not provide a GraphQL configuration, this property is ignored. |
| Method | Description |
|---|---|
init(): Promise<void> | Runs module.init(), then builds the GraphQL and Fastify servers from the module and the options. |
start(): Promise<void> | Runs onStart, binds the HTTP server to http.address and installs signal handlers that run stop() and then onShutdown. |
stop(): Promise<void> | Closes HTTP (draining open requests), disposes GraphQL, then closes the module. |
getYogaInstance(): GraphQLInstance<ServerContext, any> | undefined | The GraphQL server, or undefined before init() or when the module serves no GraphQL. |
CompositeModuleSeveral modules served as one.
import { CompositeModule } from "@evolve-framework/core";Extends AbstractModule<TContext>
new CompositeModule(modules: AbstractModule<NoInfer<TContext>>[], options?: CompositeModuleOptions)| Option | Type | Description |
|---|---|---|
graphql? | { mergeTypeDefsOptions?: { useSchemaDefinition?: boolean | undefined; throwOnConflict?: boolean | undefined; reverseDirectives?: boolean | undefined; } | undefi… |
|
HealthModuleReports this service's health as one field of a Health entity shared with every other service in the composition.
import { HealthModule } from "@evolve-framework/core";Compose it into each service that should be reportable -- every instance of every service, since the check answers for the instance that received the query -- giving each one its own field name:
new HealthModule({
name: "products",
checks: {
engine: () => client.ping(),
cache: () => cache.get("health"),
},
});
A single query then reports the whole deployment, which is what makes this usable as a promotion gate:
{ health { products { ok release } checkout { ok release } } }
Two HTTP routes carry the same information for callers that are not the router, and they answer different questions:
/healthcheckruns no checks and answers 200 with the component name and the running release for as long as the process can serve a request. This is the liveness probe, and the reason it checks nothing is that a probe fails on a timeout as surely as on a status code./readinessruns the checks and answers 503 when any of them failed. This is the one for a startup probe and for a deploy gate that can only read a status code.
A service that serves no GraphQL at all -- a payment provider, a sitemap
builder, a search indexer -- composes this with graphql: false and gets the
routes without becoming a subgraph.
This is deliberately not the router's own liveness or readiness probe: those answer for the router's HTTP layer and its selected supergraph, never reach a service, and keep reporting healthy while a service behind them is broken.
The report carries the running build so a gate can tell it reached the instances it is about to promote rather than the ones still serving traffic.
Extends AbstractModule<unknown>
new HealthModule(options: HealthModuleOptions)| Option | Type | Description |
|---|---|---|
name | string | The field this service contributes to the shared |
release? | string | The build this instance runs, reported as |
check? | HealthCheck | Whether the service itself is healthy, for a reason that is not one of its
dependencies. Its failure message lands on |
checks? | Record<string, HealthCheck | HealthCheckDefinition> | The dependencies to check, keyed by the name to report them under. They run concurrently on every query. MoreA bare function is reported without counting towards the verdict. Wrap it
as
|
readinessRoute? | string | false | Where to serve the full report over plain HTTP, or MoreIt runs the checks and answers 200 when the service is healthy, 503 when it is not -- so unlike the GraphQL query it can be gated on a status code, where a federated response reports a partial failure as a 200 with a populated errors list. Point startup probes and deploy gates here. Not a liveness probe. It reports on the service's dependencies, so a
dependency outage answers 503 and would have every instance restarted for a
fault none of them can fix. Liveness belongs on A readiness probe is worth a thought too, for the same reason in a quieter form: an outage of something every instance shares takes the whole deployment out of rotation, which turns a degraded service into an absent one. What this route is unambiguously right for is a startup probe -- invalid credentials or a missing scope then keep a new build from ever receiving traffic, which is the failure it can actually rule out. |
livenessRoute? | string | false | Where to serve the cheap liveness report, or MoreIt answers 200 with the component name and the running release for as long as this process can serve a request, and deliberately runs no checks: a probe fails on a timeout as surely as on a status code, so a route meant to say "this process is alive" must not wait on anything that can hang. |
graphql? | boolean | Whether to contribute the MoreSet it to |
timeout? | number | How long a single check may take before it counts as failed, in milliseconds. Defaults to 2000. MoreA check that never settles is the case this exists for: without it a hung dependency hangs the health query itself, and a deploy gate waiting on the query learns nothing until the router's own timeout fires. |
ProcessManagerRuns a service's start and shuts it down on SIGTERM or SIGINT: it awaits stop, flushes queued error reports and exits with 0.
import { ProcessManager } from "@evolve-framework/core";new ProcessManager(args: Args)| Option | Type | Description |
|---|---|---|
start | () => Promise<void> | |
stop? | (() => Promise<void>) |
| Method | Description |
|---|---|
start(): Promise<void> |
Functions
createApplicationConvenience factory: constructs an Application and awaits init().
import { createApplication } from "@evolve-framework/core";The TUserContext generic is captured here so the options literal is
checked against ApplicationOptions<TUserContext> — this is where
plugin-type-safety against a specific context shape is enforced. The
returned Application is non-generic.
function createApplication<TUserContext>( options: ApplicationOptions<TUserContext>,): Promise<Application>| Option | Type | Description |
|---|---|---|
name | string | The name of the domain service, used for identification and logging. |
module | Module<TUserContext> | The module that encapsulates the domain's business logic, GraphQL schema, and resolvers. This is a required property as the domain service relies on the module to define its functionality. |
http | HTTPServerOptions | HTTP server configuration. Required to start the HTTP server that handles both GraphQL and REST requests. |
onStart? | (() => void | Promise<void>) | Sets up what the modules share, such as the cache connection, when
|
onShutdown? | (() => void | Promise<void>) | Releases what a finished |
graphql? | GraphQLOptions<TUserContext> | Optional GraphQL server configuration. This is only used if the module provides a GraphQL configuration. If the module does not provide a GraphQL configuration, this property is ignored. |
Returns Promise<Application>
Types
ApplicationOptionsConfiguration options for creating an application.
import { type ApplicationOptions } from "@evolve-framework/core";export type ApplicationOptions<TUserContext extends Record<string, unknown> = Record<string, never>> = { name: string; module: Module<TUserContext>; onStart?: () => Promise<void> | void; onShutdown?: () => Promise<void> | void; graphql?: GraphQLOptions<TUserContext>; http: HTTPServerOptions;};HealthCheckOne thing a service depends on, checked while it reports its own health.
import { type HealthCheck } from "@evolve-framework/core";Returning false marks the dependency unhealthy; so does throwing, and the
error message is reported. Anything else -- including returning nothing --
counts as healthy, so a bare read of the dependency is a complete check.
export type HealthCheck = () => unknown;HttpConfigThe HTTP routes a module adds to the service, and the plugins scoped to them.
import { type HttpConfig } from "@evolve-framework/core";export type HttpConfig = { routes: (app: FastifyInstance) => void; plugins?: [FastifyPluginCallback, FastifyPluginOptions][];};InternalEventConfigWhat a module answers to on the internal event bus.
import { type InternalEventConfig } from "@evolve-framework/core";Internal events are the vendor-agnostic messages evolve services publish to
each other — a category created, content modified, a payment authorized —
each carrying a type, an origin and a timestamp, with one schema per
type in @evolve-framework/schemas. They are deliberately not a provider's
own delivery: a commercetools subscription, a Storyblok webhook or an Adyen
notification arrives as HTTP on a route (getHttpConfig()) in whatever shape
that provider sends, and the module owning that provider is what turns one
into an internal event by publishing it.
So which commerce engine a category came from is not knowable from the event, and that is the point: a CMS module creating a page for a new category works unchanged behind commercetools or Medusa, and a storefront invalidating its cache on modified content does not care which CMS moved.
export type InternalEventConfig = { handles: readonly string[]; handler: MessageHandler;};ModuleOne part of a service: the GraphQL schema and resolvers, the HTTP routes and the internal events it contributes.
import { type Module } from "@evolve-framework/core";export interface Module<TContext = unknown> { getGraphQLConfig(): GraphQLConfig | undefined; getGraphQLSchema(): string | undefined; getHttpConfig(): HttpConfig | undefined; readonly dataLoaders?: DataLoaderDefinition<TContext>; init?(): Promise<void> | void; close?(): Promise<void> | void; getInternalEventConfig?(): InternalEventConfig | undefined;}@evolve-framework/core/graphql/plugins
GraphQL server plugins for the client context and error logging.
Functions
useClientContextA Yoga plugin that settles the request's clientContext before resolvers run: it hands over the x-access-token header, if any, and awaits load().
import { useClientContext } from "@evolve-framework/core/graphql/plugins";function useClientContext(): Plugin<ClientContextPlugin>Returns Plugin<ClientContextPlugin>
useErrorLoggingLog GraphQL errors with the operation that produced them.
import { useErrorLogging } from "@evolve-framework/core/graphql/plugins";GraphQL reports execution failures inside a 200 response, and Yoga only logs the ones it considers unexpected. Without this an error like a union that fails to resolve its type leaves no trace at all: the request log line says 200 and the field is simply null.
Errors carrying serviceName or code extensions are skipped — those come
from a downstream service that has already logged them itself.
function useErrorLogging(): PluginReturns Plugin
@evolve-framework/core/messaging
The messaging port: the publisher and handler types, plus a Redis queue and a local development listener.
Classes
NoopPublisherA no-op publisher that logs and discards every message.
import { NoopPublisher } from "@evolve-framework/core/messaging";new NoopPublisher()| Method | Description |
|---|---|
publish(
message: Message,
options?: PublishMessageOptions,
): Promise<void> | Hand a message to the provider. |
RedisPublisherPublishes internal events onto a Redis list, for local development, where startDevelopmentListener reads them back.
import { RedisPublisher } from "@evolve-framework/core/messaging";new RedisPublisher(target: string)| Method | Description |
|---|---|
publish(
data: Message,
_options?: PublishMessageOptions,
): Promise<void> | Hand a message to the provider. |
Functions
isRedisQueueWhether target is a redis: or rediss: URL.
import { isRedisQueue } from "@evolve-framework/core/messaging";function isRedisQueue(target: string): booleanReturns boolean
startDevelopmentListenerPops messages from the Redis list target names and hands each, parsed as JSON, to callback, for running a consumer locally.
import { startDevelopmentListener } from "@evolve-framework/core/messaging";function startDevelopmentListener<T>( target: string, callback: (event: T) => Promise<void>, options?: Options,): Promise<void>| Option | Type | Description |
|---|---|---|
continueOnError? | boolean | |
dlq? | string |
Returns Promise<void>
Types
MessageA message as published and handled: a JSON object.
import { type Message } from "@evolve-framework/core/messaging";export type Message = Record<string, unknown>;MessageHandlerThe consume-side port.
import { type MessageHandler } from "@evolve-framework/core/messaging";export type MessageHandler<T = Message> = (data: T) => Promise<void>;MessagePublisherThe publish-side port.
import { type MessagePublisher } from "@evolve-framework/core/messaging";export type MessagePublisher = { publish(message: Message, options?: PublishMessageOptions): Promise<void>; close?(): Promise<void>;};PublishMessageOptionsDelivery metadata a publisher maps onto its provider's own fields.
import { type PublishMessageOptions } from "@evolve-framework/core/messaging";export type PublishMessageOptions = { messageGroupId?: string; origin?: string; type?: string;};@evolve-framework/core/storage
The file storage port, the signed-URL port a browser uploads and downloads through, and a local-disk implementation for development.
Classes
LocalStorageClientStores files on the local disk, as <path>/<bucket>/<key>, for development and tests.
import { LocalStorageClient } from "@evolve-framework/core/storage";A bucket or key that would land outside path, such as ../x, is refused:
the cloud adapters treat both as plain object names, so a key that walks up
the directory tree is a mistake and not a location.
new LocalStorageClient(path?: string)| Method | Description |
|---|---|
save(
bucket: string,
key: string,
data: string,
_options?: SaveOptions,
): Promise<void> | Writes data as the object key in bucket, replacing what was stored there, with options.ContentType as its content type. |
Functions
configureStorageClientResolves local filesystem storage; use a cloud adapter package otherwise.
import { configureStorageClient } from "@evolve-framework/core/storage";function configureStorageClient(target: string): StorageClientReturns StorageClient
Types
SaveOptions
import { type SaveOptions } from "@evolve-framework/core/storage";export type SaveOptions = { ContentType?: string;};SignedUploadOptions
import { type SignedUploadOptions } from "@evolve-framework/core/storage";export type SignedUploadOptions = SignedUrlOptions & { contentType: string;};SignedUrlOptionsWhat a signed URL is good for, and for how long.
import { type SignedUrlOptions } from "@evolve-framework/core/storage";export type SignedUrlOptions = { expiresInSeconds: number;};SignedUrlStorageClientA store that hands out time-limited URLs to one object, so a browser uploads or downloads it directly rather than through the service.
import { type SignedUrlStorageClient } from "@evolve-framework/core/storage";export interface SignedUrlStorageClient { signUpload: (bucket: string, key: string, options: SignedUploadOptions) => Promise<string>; signRead: (bucket: string, key: string, options: SignedUrlOptions) => Promise<string>; stat: (bucket: string, key: string) => Promise<StoredObject | undefined>;}StorageClientA store that writes objects by bucket and key.
import { type StorageClient } from "@evolve-framework/core/storage";export interface StorageClient { save: (bucket: string, key: string, data: string, options?: SaveOptions) => Promise<void>;}StoredObjectWhat a store knows of an object it holds.
import { type StoredObject } from "@evolve-framework/core/storage";export type StoredObject = { contentType?: string; size?: number;};@evolve-framework/core/service/graphql
The GraphQL request context, the store context and the session and authentication guards resolvers use.
Classes
SessionExpiredErrorThrown when the commerce platform's access token has expired, or is within the safety buffer of expiring.
import { SessionExpiredError } from "@evolve-framework/core/service/graphql";Extends GraphQLError
new SessionExpiredError(message?: string)Functions
useClientContextA Yoga plugin that settles the request's clientContext before resolvers run: it hands over the x-access-token header, if any, and awaits load().
import { useClientContext } from "@evolve-framework/core/service/graphql";function useClientContext(): Plugin<ClientContextPlugin>Returns Plugin<ClientContextPlugin>
applyFieldResolversWraps each field resolver so a parent's own __fieldResolvers entry for the field answers instead, when it has one.
import { applyFieldResolvers } from "@evolve-framework/core/service/graphql";function applyFieldResolvers<T>(resolvers: T): TReturns T
assertActiveSessionThrows SESSION_REQUIRED (HTTP 401) unless the client has a session token.
import { assertActiveSession } from "@evolve-framework/core/service/graphql";function assertActiveSession(context: ContextValue): void| Option | Type | Description |
|---|---|---|
clientContext | { isAuthenticated(): boolean; hasToken(): boolean; } |
Returns void
assertAuthenticatedThrows UNAUTHENTICATED (HTTP 401) unless the client is signed in.
import { assertAuthenticated } from "@evolve-framework/core/service/graphql";function assertAuthenticated(context: ContextValue): void| Option | Type | Description |
|---|---|---|
clientContext | { isAuthenticated(): boolean; hasToken(): boolean; } |
Returns void
assertNotAuthenticatedThrows ALREADY_AUTHENTICATED (HTTP 403) when the client is signed in.
import { assertNotAuthenticated } from "@evolve-framework/core/service/graphql";function assertNotAuthenticated(context: ContextValue): void| Option | Type | Description |
|---|---|---|
clientContext | { isAuthenticated(): boolean; hasToken(): boolean; } |
Returns void
createRequestContextReads the client address from the X-Client-IP header, which the router fills from the X-Forwarded-For chain the ingress hands over.
import { createRequestContext } from "@evolve-framework/core/service/graphql";Only the right end of the chain is the platform's: a caller can put anything in front of it, so keying a rate limit on the whole chain or its first entry would give a caller a fresh bucket per request. This relies on the service being reachable through the router only.
function createRequestContext( request: Request, options?: RequestContextOptions,): RequestContext| Option | Type | Description |
|---|---|---|
trustedHops? | number | How many entries at the right end of X-Client-IP the platform's own
proxies appended, the client's own address included. |
Returns RequestContext
createZodGraphqlErrorA helper function to create a GraphQLError with the correct extensions for the GraphQL Server.
import { createZodGraphqlError } from "@evolve-framework/core/service/graphql";function createZodGraphqlError(...issues: $ZodIssue[]): GraphQLErrorReturns GraphQLError
readStoreContextFromRequestRead the store context from the request HTTP headers.
import { readStoreContextFromRequest } from "@evolve-framework/core/service/graphql";If not all required headers are present, an error is thrown. The market,
X-StoreContext-Country, is optional and read upper-cased; one that is not
a two-letter country code is an error too.
A service that carries more than the currency, locale and store key on its store context passes a reader per own field. They are read from the request the same way and end up on the returned store context:
const storeContext = readStoreContextFromRequest(request, {
channel: {
header: "X-StoreContext-Channel",
read: (value) => value ?? "web",
},
});
// StoreContext & { channel: string }
function readStoreContextFromRequest<ExtraFields>( request: Request, extraFields?: { [Field in keyof ExtraFields]: StoreContextFieldReader<ExtraFields[Field]>; },): StoreContext & ExtraFieldsReturns StoreContext & ExtraFields
throwInternalErrorRethrows err as an INTERNAL_SERVER_ERROR GraphQL error that keeps it as originalError, with its message, or "Internal error" for a non-Error.
import { throwInternalError } from "@evolve-framework/core/service/graphql";function throwInternalError(err: unknown): neverReturns never
validateZodSchemaParses data with schema and returns the result.
import { validateZodSchema } from "@evolve-framework/core/service/graphql";function validateZodSchema<T>( schema: T, data: unknown,): z.infer<T>Returns z.infer<T>
Types
ClientContextThe per-request session an engine keeps.
import { type ClientContext } from "@evolve-framework/core/service/graphql";export interface ClientContext { isAuthenticated(): boolean; setAccessToken(token: string): void; load(): Promise<void>;}ContextValueThe GraphQL context every resolver receives; packages extend it.
import { type ContextValue } from "@evolve-framework/core/service/graphql";export interface ContextValue { loaders?: Record<string, unknown>; storeContext: StoreContext; clientContext: ClientContext; request: RequestContext; [key: string]: unknown;}RequestContextThe incoming request, with the client address read from X-Client-IP.
import { type RequestContext } from "@evolve-framework/core/service/graphql";export type RequestContext = Request & { ipAddress: string;};ServerContextBase server context type that is passed to all GraphQL resolvers.
import { type ServerContext } from "@evolve-framework/core/service/graphql";export type ServerContext = Record<string, unknown>;StoreContextThe store a request is made in, from its X-StoreContext-* headers.
import { type StoreContext } from "@evolve-framework/core/service/graphql";export type StoreContext = { currency: string; locale: string; storeKey: string; country?: string;};TypeWithInternalFieldsA resolver parent that can carry its own field resolvers under __fieldResolvers, keyed by its __typename, for applyFieldResolvers.
import { type TypeWithInternalFields } from "@evolve-framework/core/service/graphql";export type TypeWithInternalFields<T, R> = { __instance?: unknown; __fieldResolvers?: R[ExtractTypeName<T> extends keyof R ? ExtractTypeName<T> : never];} & T;@evolve-framework/core/service/http
The HTTP server underneath an application.
Functions
fastifyLoggerFastify plugin to log incoming requests and outgoing responses.
import { fastifyLogger } from "@evolve-framework/core/service/http";function fastifyLogger( instance: FastifyInstance<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, FastifyBaseLogger, FastifyTypeProviderDefault>, opts: FastifyPluginOptions, done: (err?: Error | undefined) => void,): voidReturns void
startHttpServerListens on address, resolving once the server accepts connections, so a close() that follows is guaranteed to find it listening.
import { startHttpServer } from "@evolve-framework/core/service/http";function startHttpServer( server: Server<typeof IncomingMessage, typeof ServerResponse> | FastifyInstance<Server<typeof IncomingMessage, typeof ServerResponse>, IncomingMessage, ServerResponse<IncomingMessage>, FastifyBaseLogger, FastifyTypeProviderDefault>, address: Address,): Promise<void>| Option | Type | Description |
|---|---|---|
host | string | |
port | number |
Returns Promise<void>
Types
HttpServer
import { type HttpServer } from "@evolve-framework/core/service/http";export type HttpServer = FastifyInstance<Server>;@evolve-framework/core/cache
The Redis-backed cache with its in-memory tier.
Classes
LocalTierStoreA store that answers from memory before asking the store it wraps.
import { LocalTierStore } from "@evolve-framework/core/cache";Every read a Keyv store performs is a network round trip, and a dataloader batch of one key is still a round trip. For data that a handful of keys covers and that barely changes — product types, the attributes highlighted on them, categories, the store itself — that round trip is the entire cost of the lookup: the value was already there, it just had to travel. This tier removes the travel for values that were read recently, and asks the wrapped store only for the keys it does not have.
What is held in memory is the encoded entry, exactly as the wrapped store returned it, not a deserialized value. Two things follow, and both matter:
- Keyv decodes and checks expiry above this layer, so an entry can never be served past the expiry it was written with. The local ttl only decides how long a value may be answered from memory; it cannot extend its life.
- Each caller gets a value decoded from scratch, so nothing here can be mutated by one caller and observed by another.
The cost is that eviction is local. delete reaches the wrapped store, so
other instances see it, but their own memory is untouched and keeps answering
from it until the local ttl runs out. Anything that must be uniformly fresh
the moment it changes does not belong behind this tier; everything else trades
a bounded window of staleness for not going over the network at all.
Extends EventEmitter<any>
new LocalTierStore(delegate: KeyvStoreAdapter, options: LocalTierOptions)| Option | Type | Description |
|---|---|---|
ttl | number | How long a value may be served from memory, in milliseconds. This bounds how long an eviction on another instance goes unnoticed, so it wants to be short even when the value behind it lives much longer. |
max? | number | Maximum number of entries held in memory. |
| Method | Description |
|---|---|
get<Value>(key: string): Promise<StoredData<Value>> | |
getMany<Value>(
keys: string[],
): Promise<StoredData<Value | undefined>[]> | |
set(key: string, value: any, ttl?: number | undefined): Promise<any> | |
setMany(
values: { key: string; value: any; ttl?: number | undefined; }[],
): Promise<void> | |
delete(key: string): Promise<boolean> | |
deleteMany(keys: string[]): Promise<boolean> | |
clear(): Promise<void> | |
has(key: string): Promise<boolean> | |
disconnect(): Promise<void> |
Functions
createKeyvMemoryStoreReturns a Keyv store that keeps its entries in this process.
import { createKeyvMemoryStore } from "@evolve-framework/core/cache";Nothing is shared between instances, so anything that reads a cache another instance wrote — an invalidation in particular — behaves differently than it does against Redis. That makes this a store for tests and for a local run with no Redis to point at, not a fallback for a deployment.
function createKeyvMemoryStore<T>( namespace: string, options?: KeyvRedisStoreOptions,): Keyv<T>| Option | Type | Description |
|---|---|---|
localTtl? | number | When set, values are additionally held in memory for this many
milliseconds, so a repeated read costs nothing rather than a round trip.
Only for data that tolerates being briefly stale after another instance
changes it — see |
Returns Keyv<T>
createKeyvRedisStoreReturns a Keyv store that uses Redis as a backend.
import { createKeyvRedisStore } from "@evolve-framework/core/cache";function createKeyvRedisStore<T>( redisClient: RedisClientType<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping> | undefined, namespace: string, options?: KeyvRedisStoreOptions,): Keyv<T>| Option | Type | Description |
|---|---|---|
localTtl? | number | When set, values are additionally held in memory for this many
milliseconds, so a repeated read costs nothing rather than a round trip.
Only for data that tolerates being briefly stale after another instance
changes it — see |
Returns Keyv<T>
createRateLimitStoreAdapts a client from createRedisClient for RedisStore from @envelop/rate-limiter: new RedisStore(createRateLimitStore(cache.client)).
import { createRateLimitStore } from "@evolve-framework/core/cache";It fails open. Each command is bounded by the cache's timeout, a read that fails or times out answers no earlier calls, and a write that does is dropped, both with a throttled warning. After a failure Redis is left alone for a few seconds, so a limit check during an outage answers at once. Limits are not enforced while Redis is away, which is the price of a checkout that keeps working through a failover.
function createRateLimitStore( client: RedisClientType<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping>, timeout?: number, retryAfter?: number,): RateLimitRedisClientReturns RateLimitRedisClient
createRedisClientcreate a new redis.RedisClient instance
import { createRedisClient } from "@evolve-framework/core/cache";A number of settings are passed to the constructor to ensure that the client handles network issues gracefully.
Authentication that requires a cloud-specific SDK (for example Microsoft
Entra ID for Azure Managed Redis) is not handled here. Instead the caller
injects a credentialsProvider built by the relevant cloud adapter (e.g.
createEntraIdCredentialsProvider from @evolve-framework/cloud-adapter-azure).
This keeps core free of any cloud provider SDK.
function createRedisClient( redisUrl: string | undefined, credentialsProvider?: CredentialsProvider,): Promise<RedisClient | undefined>| Option | Type | Description |
|---|---|---|
type | "async-credentials-provider" | |
credentials? | () => Promise<BasicAuth> | |
subscribe? | (listener: StreamingCredentialsListener<BasicAuth>) => Promise<[BasicAuth, Disposable]> | Provides initial credentials and subscribes to subsequent updates. This is used internally by the node-redis client to handle credential rotation and re-authentication. MoreNote: The node-redis client manages the subscription lifecycle automatically. Users only need to implement onReAuthenticationError if they want to be notified about authentication failures. Error handling:
|
onReAuthenticationError? | (error: ReAuthenticationError) => void | Called when authentication fails or credentials cannot be renewed in time. Implement this to handle authentication errors in your application. |
Returns Promise<RedisClient | undefined>
Constants
cacheThe process-wide cache.
import { cache } from "@evolve-framework/core/cache";const cache: CacheHandlerTypes
CredentialsProviderA Redis credentials provider, as accepted by the underlying @redis/client.
import { type CredentialsProvider } from "@evolve-framework/core/cache";Cloud adapters build a concrete provider that authenticates against a
specific platform — e.g. createEntraIdCredentialsProvider from
@evolve-framework/cloud-adapter-azure for Azure Managed Redis — and the
application injects it via cache.configure. Keeping this as the public port
type means core never depends on a cloud provider SDK.
export type CredentialsProvider = NonNullable<RedisClientOptions["credentialsProvider"]>;RateLimitRedisClientThe callback client RedisStore from @envelop/rate-limiter drives.
import { type RateLimitRedisClient } from "@evolve-framework/core/cache";export type RateLimitRedisClient = { get(key: string, callback: Callback<string>): void; set(args: Array<string | number>, callback: (error: Error | null) => void): void;};RedisClientThe node-redis client createRedisClient returns.
import { type RedisClient } from "@evolve-framework/core/cache";export type RedisClient = ReturnType<typeof createClient>;@evolve-framework/core/utils
Building blocks for per-request dataloaders, and file utilities for build scripts.
Functions
copyFilesReturn a function that copies files to the specified directories.
import { copyFiles } from "@evolve-framework/core/utils";function copyFiles(...directories: string[]): (...files: string[]) => voidReturns (...files: string[]) => void
createDeferredDataLoadersCreates request-scoped DataLoaders that are initialized only when first used.
import { createDeferredDataLoaders } from "@evolve-framework/core/utils";function createDeferredDataLoaders<TLoaders>( initializers: { [K in keyof TLoaders]: () => TLoaders[K]; },): TLoadersReturns TLoaders
defineDataLoadersDeclares a set of loaders that a module contributes to the request context.
import { defineDataLoaders } from "@evolve-framework/core/utils";Curried so that TContext can be given explicitly while the loader names and
their types are still inferred from the object literal.
function defineDataLoaders<TContext>(): <TInitializers extends Record<string, (context: TContext) => unknown>>(initializers: TInitializers) => DataLoaderDefinition<TContext, { [K in keyof TInitializers]: ReturnType<TInitializers[K]>; }>Returns <TInitializers extends Record<string, (context: TContext) => unknown>>(initializers: TInitializers) => DataLoaderDefinition<TContext, { [K in keyof TInitializers]: ReturnType<TInitializers[K]>; }>
mergeDataLoaderDefinitionsCombines the loader definitions a composition contributes into one context.loaders.
import { mergeDataLoaderDefinitions } from "@evolve-framework/core/utils";Definitions are deduplicated by identity, so the twelve modules of a single package may all point at the same module-scoped definition without that counting as a conflict. Two different definitions claiming the same name is a hard error: the names are a flat namespace and there is no way to tell which implementation a resolver meant. Silently letting one win is how the bug this design came from stayed invisible.
Nothing is constructed here. A definition's create runs on the first read
of any loader it owns, and the loaders it returns are each built on first
use.
function mergeDataLoaderDefinitions<TContext>( definitions: Iterable<DataLoaderDefinition<TContext, Record<string, unknown>>>, context: TContext,): Record<string, unknown>Returns Record<string, unknown>
overlayDataLoadersLays one set of loaders over another, with the overlay winning on any shared name.
import { overlayDataLoaders } from "@evolve-framework/core/utils";Only needed while services still assemble loaders by hand: the modules contribute their declarations and whatever the service already put on the context keeps precedence, so a service can migrate one module at a time instead of in a single step. Both sets are enumerable, so this is a plain key merge — no proxy juggling, and still nothing is constructed until read.
function overlayDataLoaders( base: Record<string, unknown>, overlay: Record<string, unknown>,): Record<string, unknown>Returns Record<string, unknown>
Types
DataLoaderDefinitionA set of loaders a module contributes, declared once at module scope.
import { type DataLoaderDefinition } from "@evolve-framework/core/utils";Declaration and construction are deliberately separate: keys is known as
soon as the module is imported, so a composition can be checked for
duplicates before a request is ever served, while create is only called
per request and hands back loaders that are each still built on first use.
export type DataLoaderDefinition<TContext, TLoaders extends Record<string, unknown> = Record<string, unknown>> = { readonly keys: readonly string[]; readonly create: (context: TContext) => TLoaders;};LoadersOfThe loaders a declaration — or a composition — produces.
import { type LoadersOf } from "@evolve-framework/core/utils";A service that wants a typed context.loaders for its resolvers builds it
from the declarations rather than asserting a shape:
type Loaders = LoadersOf<typeof commercetoolsDataLoaders> &
LoadersOf<typeof myModuleDataLoaders>;
Adding a module to the composition and forgetting it here leaves its loaders untyped rather than missing, so this stays a reading convenience — what is actually present at runtime is decided by the modules, not by this type.
export type LoadersOf<TDefinition> = TDefinition extends DataLoaderDefinition<any, infer TLoaders> ? TLoaders : never;@evolve-framework/core/http/client
A fetch-based HTTP client with retries and error helpers.
Functions
fetchClientCustom fetch function
import { fetchClient } from "@evolve-framework/core/http/client";function fetchClient( url: string | Request | URL, options?: RequestInit, spanName?: string,): Promise<Response>Returns Promise<Response>
isHttpErrorWhether error is an Error that carries an HTTP statusCode.
import { isHttpError } from "@evolve-framework/core/http/client";function isHttpError(error: unknown): error is HttpErrorReturns error is HttpError
joinURLJoins URL parts with single slashes, as url-join does.
import { joinURL } from "@evolve-framework/core/http/client";function joinURL(...parts: string[]): stringReturns string
retrywill retry the fn in case of an exception with exponential backoff;
import { retry } from "@evolve-framework/core/http/client";function retry<T, E>( fn: () => Promise<T>, options?: Options<T, E>,): Promise<T>| Option | Type | Description |
|---|---|---|
maxNumberOfAttempts? | number | |
baseDelay? | number | |
maxDelay? | number | |
errorHandler? | ((error: E) => () => Promise<T>) |
Returns Promise<T>
@evolve-framework/core/observability
The logger, tracer and meter.
Functions
getActiveSpanThe span of the current async context, if any.
import { getActiveSpan } from "@evolve-framework/core/observability";function getActiveSpan(): Span | undefinedReturns Span | undefined
resourceSpanNameName a span that wraps a logical call to a vendor.
import { resourceSpanName } from "@evolve-framework/core/observability";The transport span under it already carries the method and the host, so a wrapper named after the request path only restates it -- and truncating that path to keep ids out of the name collapses unrelated calls together. The name says which resource was addressed; the request itself belongs in the span's attributes.
segments are the meaningful path segments, in order, with whatever scopes
a request rather than names a resource already removed -- a project key, an
API version, an in-store prefix. The first is the resource; anything after
it selects one, so the name is singular.
function resourceSpanName( vendor: string, method: string, segments: readonly string[],): stringReturns string
setLogDestinationDirects log output to stdout or stderr.
import { setLogDestination } from "@evolve-framework/core/observability";A process whose stdout carries a protocol -- an MCP server speaking JSON-RPC over stdio, for instance -- must move its logs to stderr, because a log line interleaved into that stream corrupts the protocol. Call this before the first log line is written; anything already flushed stays where it went.
function setLogDestination(destination: LogDestination): voidReturns void
urlResourceSegmentsThe meaningful path segments of a URL, for a caller with no vendor-specific knowledge of the path.
import { urlResourceSegments } from "@evolve-framework/core/observability";function urlResourceSegments(path: string): string[]Returns string[]
Constants
contextThe per-request logger store logger reads from once withLoggerContext has enabled it.
import { context } from "@evolve-framework/core/observability";const context: { store: AsyncLocalStorage<Map<string, Logger>>; enabled: boolean; }loggerThe logger to be used by all applications.
import { logger } from "@evolve-framework/core/observability";const logger: LoggermeterThe OpenTelemetry meter, named after SERVICE_NAME.
import { meter } from "@evolve-framework/core/observability";const meter: MetertracerInstrumentation scope for the spans the framework itself emits — the commercetools client middleware, the cache handler, the Lambda wrapper.
import { tracer } from "@evolve-framework/core/observability";A scope names the code doing the instrumenting, not the service being
instrumented; the service already travels on the resource as service.name.
Naming it after SERVICE_NAME also read the variable at module load, before
initObservability() applies its own fallback, so an unset SERVICE_NAME
interpolated to the literal string "undefined".
const tracer: TracerTypes
LogDestination
import { type LogDestination } from "@evolve-framework/core/observability";export type LogDestination = "stdout" | "stderr";Logger
import { type Logger } from "@evolve-framework/core/observability";export type Logger<CustomLevels extends string = never, UseOnlyCustomLevels extends boolean = boolean> = BaseLogger & LoggerExtras<CustomLevels> & CustomLevelLogger<CustomLevels, UseOnlyCustomLevels>@evolve-framework/core/observability/configure
Start-up configuration for tracing, metrics, Sentry and event-loop monitoring.
Functions
configureEventLoopBlockDetectionReport a blocked event loop to Sentry, with the stack trace of whatever was holding it.
import { configureEventLoopBlockDetection } from "@evolve-framework/core/observability/configure";A stall is invisible in a trace. Every span that happens to be open absorbs it, so dozens of resolvers report an identical duration and none of them is the one at fault -- which is why the resolver span filter refuses to judge a span by its duration. This answers the question those spans cannot: a watchdog thread notices the main thread has stopped answering and captures where it is stuck.
On wherever there is a DSN to report to, unless SENTRY_EVENT_LOOP_BLOCK is
false. SENTRY_EVENT_LOOP_BLOCK_THRESHOLD sets how long the loop must be
blocked before an event is sent, in milliseconds.
What it costs is a watchdog thread, which is a V8 isolate of its own and measures around 35MB. The polling it asks of the main thread does not register: one poll per half threshold, at roughly 8 microseconds each, is two hundred-thousandths of a core at the default. Sentry also rate-limits itself to one such event per hour, so this reports that stalls are happening and what they look like, not how many there were.
Nothing here is allowed to prevent the service from starting. The native module resolves a prebuilt binary for the exact platform, libc and Node ABI, and throws while loading when there is no match -- on a Node release that Sentry has not built for yet, that would otherwise take down every service at startup, before any of them could report why. Now that this runs by default that is every service at once, which is why it is imported dynamically and a failure is a warning.
function configureEventLoopBlockDetection(): Promise<void>Returns Promise<void>
configureSentryConfigure Sentry and all relevant integrations.
import { configureSentry } from "@evolve-framework/core/observability/configure";This also makes sure that whenever we send an event to Sentry, we also include the current logger context.
function configureSentry({ tracing, redact }?: SentryOptions | undefined): void| Option | Type | Description |
|---|---|---|
tracing? | boolean | Overrides |
redact? | RedactOptions | Overrides the environment defaults for URL and span redaction. |
Returns void
initObservabilityInitialize error reporting and tracing before the application loads.
import { initObservability } from "@evolve-framework/core/observability/configure";This code is based on the documentation provided by Sentry, see: - https://docs.sentry.io/platforms/node/performance/instrumentation/opentelemetry/
function initObservability({ redact }?: ObservabilityOptions | undefined): void| Option | Type | Description |
|---|---|---|
redact? | RedactOptions | Overrides the environment defaults for URL and span redaction. |
Returns void
registerObservabilityStart observability for a whole process: initObservability, then the event-loop block watchdog.
import { registerObservability } from "@evolve-framework/core/observability/configure";function registerObservability( options?: ObservabilityOptions,): Promise<void>| Option | Type | Description |
|---|---|---|
redact? | RedactOptions | Overrides the environment defaults for URL and span redaction. |
Returns Promise<void>
Types
RedactOptions
import { type RedactOptions } from "@evolve-framework/core/observability/configure";export type RedactOptions = { queryParams?: string[]; paths?: string[];};@evolve-framework/core/observability/logging
Logging only: the logger, its destination and error reporting.
Functions
setErrorReporterSend log events of error level and above to an error reporting backend.
import { setErrorReporter } from "@evolve-framework/core/observability/logging";The backend registers itself here rather than being imported, so that this module carries no reporting SDK of its own. A process that only writes logs then pays neither the dependency nor the time it takes to load it, which for the Sentry SDK is close to seven hundred modules -- a cost that would otherwise fall on everything that imports a logger, including every test file. A process that does report registers the backend when it configures it.
Until a reporter is registered, error level logs are written and nothing else happens to them.
function setErrorReporter(reporter: ErrorReporter): voidReturns void
setLogDestinationDirects log output to stdout or stderr.
import { setLogDestination } from "@evolve-framework/core/observability/logging";A process whose stdout carries a protocol -- an MCP server speaking JSON-RPC over stdio, for instance -- must move its logs to stderr, because a log line interleaved into that stream corrupts the protocol. Call this before the first log line is written; anything already flushed stays where it went.
function setLogDestination(destination: LogDestination): voidReturns void
withLoggerContextRuns the given function within a new logger context.
import { withLoggerContext } from "@evolve-framework/core/observability/logging";function withLoggerContext<T>(child: Logger, fn: () => T | Promise<T>): voidReturns void
Constants
loggerThe logger to be used by all applications.
import { logger } from "@evolve-framework/core/observability/logging";const logger: LoggerrootLoggerThe root logger, of which childs can be created.
import { rootLogger } from "@evolve-framework/core/observability/logging";const rootLogger: Logger<never, boolean>Types
ErrorReportOne log event of error level or above, as handed to the error reporter.
import { type ErrorReport } from "@evolve-framework/core/observability/logging";export type ErrorReport = { level: ErrorReportLevel; message: string; error?: Error; context: Record<string, string>; fingerprint: string[];};ErrorReporterSends an error report to a backend; registered with setErrorReporter.
import { type ErrorReporter } from "@evolve-framework/core/observability/logging";export type ErrorReporter = (report: ErrorReport) => void;ErrorReportLevelThe severity of a reported log event, using the names Sentry gives them.
import { type ErrorReportLevel } from "@evolve-framework/core/observability/logging";export type ErrorReportLevel = "fatal" | "error" | "warning" | "info" | "debug";LogDestination
import { type LogDestination } from "@evolve-framework/core/observability/logging";export type LogDestination = "stdout" | "stderr";Logger
import { type Logger } from "@evolve-framework/core/observability/logging";export type Logger<CustomLevels extends string = never, UseOnlyCustomLevels extends boolean = boolean> = BaseLogger & LoggerExtras<CustomLevels> & CustomLevelLogger<CustomLevels, UseOnlyCustomLevels>@evolve-framework/core/address
The city-lookup port an address-lookup provider implements, and officialCity, which an engine applies on an address write.
Functions
officialCityThe city to store for an address: the provider's spelling where it names the same city ignoring case, so "utrecht" is stored as "Utrecht", and otherwise the input unchanged.
import { officialCity } from "@evolve-framework/core/address";function officialCity( lookup: CityLookup | undefined, address: { country?: string | null | undefined; postalCode?: string | null | undefined; streetNumber?: string | null | undefined; city?: string | null | undefined; }, options?: OfficialCityOptions,): Promise<string | undefined>| Option | Type | Description |
|---|---|---|
country? | string | null | |
postalCode? | string | null | |
streetNumber? | string | null | |
city? | string | null | |
timeoutMs? | number | How long the lookup may take before the input is kept. Defaults to 1.5 seconds. |
Returns Promise<string | undefined>
Types
CityLookupThe official name of the city at a postcode and house number, from an address-lookup provider.
import { type CityLookup } from "@evolve-framework/core/address";export type CityLookup = (query: CityLookupQuery) => Promise<string | undefined>;CityLookupQueryThe part of an address a lookup by postcode and house number needs.
import { type CityLookupQuery } from "@evolve-framework/core/address";export type CityLookupQuery = { country: string; postalCode: string; streetNumber: string;};@evolve-framework/core/order-link
Signing a link that lets a buyer read one order until it expires, and reading one back.
Functions
createOrderLinkTokenSign a link to an order, valid until expiresAt.
import { createOrderLinkToken } from "@evolve-framework/core/order-link";function createOrderLinkToken(link: OrderLink, secret: string): string| Option | Type | Description |
|---|---|---|
orderId | string | |
expiresAt | Date |
Returns string
readOrderLinkTokenThe order a token links to, or undefined when the token is malformed, signed with another secret, or past its expiry.
import { readOrderLinkToken } from "@evolve-framework/core/order-link";function readOrderLinkToken( token: string, secret: string, now?: Date,): OrderLink | undefinedReturns OrderLink | undefined
Types
OrderLinkA token that lets whoever holds it read one order until it expires, for a link in an email to a buyer who may not be signed in.
import { type OrderLink } from "@evolve-framework/core/order-link";export type OrderLink = { orderId: string; expiresAt: Date;};@evolve-framework/core/site-revalidation
The sites' cache tags, and a signed purge that a CMS publish sends to each site's revalidate route.
Functions
articlesTagEvery article, overview or topic list query, and Product.articles.
import { articlesTag } from "@evolve-framework/core/site-revalidation";function articlesTag(locale: string): stringReturns string
createSiteRevalidatorA revalidator that POSTs { tags }, signed with the shared secret, to each site URL.
import { createSiteRevalidator } from "@evolve-framework/core/site-revalidation";function createSiteRevalidator( options: SiteRevalidationOptions, logger: RevalidationLogger,): SiteRevalidator| Option | Type | Description |
|---|---|---|
urls | string[] | Each site's revalidate route, the URL its |
secret | string | The secret shared with the sites, their |
info | (entry: Record<string, unknown>) => void | |
error | (entry: Record<string, unknown>) => void | |
maxAttempts? | number | Attempts per URL before a purge is given up on. Defaults to 3. |
timeout? | number | How long one attempt may take, in milliseconds. Defaults to 5000. |
Returns SiteRevalidator
layoutTagNavigation and site settings.
import { layoutTag } from "@evolve-framework/core/site-revalidation";function layoutTag(locale: string): stringReturns string
messagesTagThe store's fixed system copy and its content snippets.
import { messagesTag } from "@evolve-framework/core/site-revalidation";function messagesTag(locale: string): stringReturns string
pagesTagEvery page fetch in a locale, for content embedded in pages nobody can list.
import { pagesTag } from "@evolve-framework/core/site-revalidation";function pagesTag(locale: string): stringReturns string
pageTagThe tag a site puts on one page fetch, page:{locale}:{path}, with the home page as /.
import { pageTag } from "@evolve-framework/core/site-revalidation";function pageTag(locale: string, path: string): stringReturns string
Types
SiteRevalidatorPurge the given cache tags on every site.
import { type SiteRevalidator } from "@evolve-framework/core/site-revalidation";export type SiteRevalidator = (tags: string[]) => Promise<void>;Deprecated
Still exported so existing code keeps working. Each one says what replaces it.
createDomainService
import { createDomainService } from "@evolve-framework/core";function createDomainService<TUserContext>( options: ApplicationOptions<TUserContext>,): Promise<Application>| Option | Type | Description |
|---|---|---|
name | string | The name of the domain service, used for identification and logging. |
module | Module<TUserContext> | The module that encapsulates the domain's business logic, GraphQL schema, and resolvers. This is a required property as the domain service relies on the module to define its functionality. |
http | HTTPServerOptions | HTTP server configuration. Required to start the HTTP server that handles both GraphQL and REST requests. |
onStart? | (() => void | Promise<void>) | Sets up what the modules share, such as the cache connection, when
|
onShutdown? | (() => void | Promise<void>) | Releases what a finished |
graphql? | GraphQLOptions<TUserContext> | Optional GraphQL server configuration. This is only used if the module provides a GraphQL configuration. If the module does not provide a GraphQL configuration, this property is ignored. |
Returns Promise<Application>
DomainService
import { DomainService } from "@evolve-framework/core";Extends Application
new DomainService(options: ApplicationOptions<any>)| Option | Type | Description |
|---|---|---|
name | string | The name of the domain service, used for identification and logging. |
module | Module<any> | The module that encapsulates the domain's business logic, GraphQL schema, and resolvers. This is a required property as the domain service relies on the module to define its functionality. |
http | HTTPServerOptions | HTTP server configuration. Required to start the HTTP server that handles both GraphQL and REST requests. |
onStart? | (() => void | Promise<void>) | Sets up what the modules share, such as the cache connection, when
|
onShutdown? | (() => void | Promise<void>) | Releases what a finished |
graphql? | GraphQLOptions<any> | Optional GraphQL server configuration. This is only used if the module provides a GraphQL configuration. If the module does not provide a GraphQL configuration, this property is ignored. |
Not listed: 8 options types. They appear as the tables under the constructors and functions they type.

