Registry and Processors
Overview
The Registry is EverShop's primary extension mechanism. It allows modules and extensions to transform data by registering processor functions that form a pipeline. When a value is requested from the registry, all registered processors execute in priority order, each receiving the output of the previous one.
Unlike hooks (which intercept function calls), processors transform data values. They are used throughout EverShop for tasks like defining cart fields, customizing email services, validating configuration schemas, and modifying product data before it is saved.
How It Works
The processor pipeline follows this flow:
Initial Value → Processor 1 → Processor 2 → ... → Processor N → Final Value
(priority 5) (priority 10) (priority 1000)
- A module calls
getValue('cartFields', [])with an initial value (an empty array). - The registry finds all processors registered for
'cartFields'. - Processors execute in priority order (lowest number first).
- Each processor receives the current value and must return the transformed value.
- The final result is returned to the caller.
Registering Processors
Use addProcessor() to register a processor in your module's bootstrap.ts file:
import { addProcessor } from '@evershop/evershop/lib/util/registry';
export default function () {
// Add a field to every cart
addProcessor('cartFields', (fields) => {
fields.push({
key: 'gift_message',
resolvers: {
// resolver configuration...
}
});
return fields;
}, 15);
}
Parameters
name(string) — The name of the registry value to process.callback((value: T) => T | Promise<T>) — The processor function. It receives the current value and must return the (possibly modified) value.priority(number, optional, default:10) — Controls execution order. Lower numbers execute first. Valid range: 0–999.
If a processor does not return a value (returns undefined), a warning is logged and subsequent processors may break. Always return the value from your processor, even if you didn't modify it.
Priority System
Processors are sorted by priority before execution:
| Priority | Purpose |
|---|---|
| 0–9 | Early processing (validation, defaults) |
| 10 | Default priority |
| 11–999 | Later processing (enrichment, transformation) |
| 1000 | Final processor (reserved, only one allowed per value) |
// Executes first — set defaults
addProcessor('productData', setDefaults, 5);
// Executes second — validate
addProcessor('productData', validate, 10);
// Executes third — enrich with external data
addProcessor('productData', enrichData, 20);
Final Processors
Use addFinalProcessor() to register a processor that always runs last (priority 1000). Only one final processor is allowed per registry value.
import { addFinalProcessor } from '@evershop/evershop/lib/util/registry';
addFinalProcessor('emailService', (service) => {
// Override the email service implementation
return myCustomEmailService;
});
Retrieving Values
Asynchronous (Recommended)
import { getValue } from '@evershop/evershop/lib/util/registry';
// Pass an initial value
const cartFields = await getValue('cartFields', []);
// Pass an initialization function instead of a static value
const productData = await getValue('productData', async (value) => {
return await loadFromDatabase();
});
Synchronous
import { getValueSync } from '@evershop/evershop/lib/util/registry';
const config = getValueSync('configurationSchema', baseSchema);
getValueSync() will throw an error if any processor is asynchronous. Use getValue() if your processors perform async operations.
Context in Processors
Processors can access a context object via this. The context is passed as the third argument to getValue():
// Passing context when retrieving a value
const finalPrice = await getValue(
'productPrice',
basePrice,
{ customer: currentCustomer, currency: 'USD' }
);
// Accessing context in a processor — use a regular function, not an arrow function
addProcessor('productPrice', function (price) {
const customer = this.customer;
if (customer && customer.isVip) {
return price * 0.9; // VIP discount
}
return price;
});
Arrow functions do not have their own this binding. If you need to access the context, use the function keyword.
Caching
The registry caches processed values. If getValue() is called again with the same name, initValue, and context, the cached result is returned without re-running processors. This makes repeated calls efficient.
The cache is cleared when the registry is locked during startup.
Lock Mechanism
Processors must be registered during the bootstrap phase, before the application starts handling requests. After all modules load their bootstrap scripts, EverShop calls lockRegistry(). Any attempt to call addProcessor() after locking throws an error:
Registry is locked. Most likely you are trying to add a processor from a middleware.
Consider using a bootstrap file to add processors.
This ensures the processor pipeline is stable and predictable during request handling.
Practical Examples
Adding Cart Fields
The checkout module uses the registry to define which fields a cart contains:
import { addProcessor } from '@evershop/evershop/lib/util/registry';
export default function () {
addProcessor('cartFields', (fields) => {
fields.push({
key: 'coupon',
resolvers: {
// How this field is read and written
}
});
return fields;
});
}
Customizing the Email Service
Extensions can override the email sending implementation:
import { addProcessor } from '@evershop/evershop/lib/util/registry';
export default function () {
addProcessor('emailService', (currentService) => {
// Replace the default email service with Resend
return {
send: async (to, subject, html) => {
await resend.emails.send({ from: 'noreply@shop.com', to, subject, html });
}
};
});
}
Extending Configuration Validation
Modules merge their own JSON schemas into the global configuration schema:
import { addProcessor } from '@evershop/evershop/lib/util/registry';
export default function () {
addProcessor('configurationSchema', (schema) => {
return {
...schema,
properties: {
...schema.properties,
myModule: {
type: 'object',
properties: {
apiKey: { type: 'string' }
}
}
}
};
});
}
Registry vs Hooks
| Aspect | Registry (Processors) | Hooks |
|---|---|---|
| Purpose | Transform data values | Intercept function calls |
| When to use | Modifying data before/after save, defining fields, configuring services | Running code before/after a specific operation |
| Registration | addProcessor(name, callback, priority) | hookBefore(funcName, callback, priority) |
| Return value | Must return the transformed value | Does not need to return anything |
| Registered in | bootstrap.ts | bootstrap.ts |
See Also
- addProcessor — Function reference
- getValue — Async value retrieval
- getValueSync — Sync value retrieval
- hookable — The hook system (an alternative extension mechanism)
Support us
EverShop is an open-source project that relies on community support. If you find our project useful, please consider sponsoring us.