Skip to main content

Custom Strategies in Plugins

When building Vendure plugins, you often need to provide extensible, pluggable implementations for specific features. The strategy pattern is the perfect tool for this, allowing plugin users to customize behavior by providing their own implementations.

This guide shows you how to implement custom strategies in your plugins, following Vendure's established patterns and best practices.

Overview

A strategy in Vendure is a way to provide a pluggable implementation of a particular feature. Custom strategies in plugins allow users to:

  • Override default behavior with their own implementations
  • Inject dependencies and services through the init() lifecycle method
  • Clean up resources using the destroy() lifecycle method
  • Configure the strategy through the plugin's init options

Creating a Strategy Interface

First, define the interface that your strategy must implement. All strategy interfaces should extend InjectableStrategy to support dependency injection and lifecycle methods.

src/strategies/my-custom-strategy.ts
import { InjectableStrategy, RequestContext } from '@vendure/core';

export interface MyCustomStrategy extends InjectableStrategy {
/**
* Process some data and return a result
*/
processData(ctx: RequestContext, data: any): Promise<string>;

/**
* Validate the input data
*/
validateInput(data: any): boolean;
}

Implementing a Default Strategy

Create a default implementation that users can extend or replace:

src/strategies/default-my-custom-strategy.ts
import { Injector, RequestContext, Logger } from '@vendure/core';
import { MyCustomStrategy } from './my-custom-strategy';
import { SomeOtherService } from '../services/some-other.service';
import { loggerCtx } from '../constants';

export class DefaultMyCustomStrategy implements MyCustomStrategy {
private someOtherService: SomeOtherService;

async init(injector: Injector): Promise<void> {
// Inject dependencies during the init phase
this.someOtherService = injector.get(SomeOtherService);

// Perform any setup logic
Logger.info('DefaultMyCustomStrategy initialized', loggerCtx);
}

async destroy(): Promise<void> {
// Clean up resources if needed
Logger.info('DefaultMyCustomStrategy destroyed', loggerCtx);
}

async processData(ctx: RequestContext, data: any): Promise<string> {
// Validate input first
if (!this.validateInput(data)) {
throw new Error('Invalid input data');
}

// Use injected service to process data
const result = await this.someOtherService.doSomething(ctx, data);
// ... do something with the result
return result;
}

validateInput(data: any): boolean {
return data != null && typeof data === 'object';
}
}

Adding Strategy to Plugin Options

Define your plugin's initialization options to include the strategy:

src/types.ts
import { MyCustomStrategy } from './strategies/my-custom-strategy';

export interface MyPluginInitOptions {
/**
* Custom strategy for processing data
* @default DefaultMyCustomStrategy
*/
processingStrategy?: MyCustomStrategy;

/**
* Other plugin options
*/
someOtherOption?: string;
}

Configuring the Plugin

In your plugin definition, provide the default strategy and allow users to override it:

src/my-plugin.ts
import { PluginCommonModule, VendurePlugin, Injector } from '@vendure/core';
import { OnApplicationBootstrap, OnApplicationShutdown } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';

import { MY_PLUGIN_OPTIONS } from './constants';
import { MyPluginInitOptions } from './types';
import { DefaultMyCustomStrategy } from './strategies/default-my-custom-strategy';
import { MyPluginService } from './services/my-plugin.service';
import { SomeOtherService } from './services/some-other.service';

@VendurePlugin({
imports: [PluginCommonModule],
providers: [
MyPluginService,
SomeOtherService,
{
provide: MY_PLUGIN_OPTIONS,
useFactory: () => MyPlugin.options,
},
],
configuration: config => {
// You can also configure core Vendure strategies here if needed
return config;
},
compatibility: '^3.0.0',
})
export class MyPlugin implements OnApplicationBootstrap, OnApplicationShutdown {
static options: MyPluginInitOptions;

constructor(private moduleRef: ModuleRef) {}

static init(options: MyPluginInitOptions) {
this.options = {
// Provide default strategy if none specified
processingStrategy: new DefaultMyCustomStrategy(),
...options,
};
return MyPlugin;
}

async onApplicationBootstrap() {
await this.initStrategy();
}

async onApplicationShutdown() {
await this.destroyStrategy();
}

private async initStrategy() {
const strategy = MyPlugin.options.processingStrategy;
if (strategy && typeof strategy.init === 'function') {
const injector = new Injector(this.moduleRef);
await strategy.init(injector);
}
}

private async destroyStrategy() {
const strategy = MyPlugin.options.processingStrategy;
if (strategy && typeof strategy.destroy === 'function') {
await strategy.destroy();
}
}
}

Using the Strategy in Services

Access the strategy through dependency injection in your services:

src/services/my-plugin.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { RequestContext } from '@vendure/core';

import { MY_PLUGIN_OPTIONS } from '../constants';
import { MyPluginInitOptions } from '../types';

@Injectable()
export class MyPluginService {
constructor(@Inject(MY_PLUGIN_OPTIONS) private options: MyPluginInitOptions) {}

async processUserData(ctx: RequestContext, userData: any): Promise<string> {
// Delegate to the configured strategy
return this.options.processingStrategy.processData(ctx, userData);
}

validateUserInput(data: any): boolean {
return this.options.processingStrategy.validateInput(data);
}
}

User Implementation Example

Plugin users can now provide their own strategy implementations:

src/my-custom-implementation.ts
import { Injector, RequestContext, Logger } from '@vendure/core';
import { MyCustomStrategy } from '@my-org/my-plugin';
import { ExternalApiService } from './external-api.service';
import { loggerCtx } from '../constants';

export class CustomProcessingStrategy implements MyCustomStrategy {
private externalApi: ExternalApiService;

async init(injector: Injector): Promise<void> {
this.externalApi = injector.get(ExternalApiService);

// Initialize external API connection
await this.externalApi.connect();
Logger.info('Custom processing strategy initialized', loggerCtx);
}

async destroy(): Promise<void> {
// Clean up external connections
if (this.externalApi) {
await this.externalApi.disconnect();
}
Logger.info('Custom processing strategy destroyed', loggerCtx);
}

async processData(ctx: RequestContext, data: any): Promise<string> {
if (!this.validateInput(data)) {
throw new Error('Invalid data format');
}

// Use external API for processing
const result = await this.externalApi.processData(data);
return `Processed: ${result}`;
}

validateInput(data: any): boolean {
// Custom validation logic
return data && data.type === 'custom' && data.value;
}
}

Plugin Configuration by Users

Users configure the plugin with their custom strategy:

vendure-config.ts
import { VendureConfig } from '@vendure/core';
import { MyPlugin } from '@my-org/my-plugin';
import { CustomProcessingStrategy } from './my-custom-implementation';

export const config: VendureConfig = {
plugins: [
MyPlugin.init({
processingStrategy: new CustomProcessingStrategy(),
someOtherOption: 'custom-value',
}),
],
// ... other config
};

Strategy with Options

You can also create strategies that accept configuration options:

src/strategies/configurable-strategy.ts
import { Injector, RequestContext } from '@vendure/core';
import { MyCustomStrategy } from './my-custom-strategy';

export interface ConfigurableStrategyOptions {
timeout: number;
retries: number;
apiKey: string;
}

export class ConfigurableStrategy implements MyCustomStrategy {
constructor(private options: ConfigurableStrategyOptions) {}

async init(injector: Injector): Promise<void> {
// Use options during initialization
console.log(`Strategy configured with timeout: ${this.options.timeout}ms`);
}

async destroy(): Promise<void> {
// Cleanup logic
}

async processData(ctx: RequestContext, data: any): Promise<string> {
// Use configuration options
const timeout = this.options.timeout;
const retries = this.options.retries;

// Implementation using these options...
return 'processed with options';
}

validateInput(data: any): boolean {
return true;
}
}

Usage:

vendure-config.ts
import { ConfigurableStrategy } from './strategies/configurable-strategy';

// In plugin configuration
MyPlugin.init({
processingStrategy: new ConfigurableStrategy({
timeout: 5000,
retries: 3,
apiKey: process.env.API_KEY,
}),
});

Multiple Strategies in One Plugin

For complex plugins, you might need multiple strategies:

src/types.ts
export interface ComplexPluginOptions {
dataProcessingStrategy?: DataProcessingStrategy;
validationStrategy?: ValidationStrategy;
cacheStrategy?: CacheStrategy;
}
src/complex-plugin.ts
@VendurePlugin({
// ... plugin config
})
export class ComplexPlugin implements OnApplicationBootstrap, OnApplicationShutdown {
static options: ComplexPluginOptions;

static init(options: ComplexPluginOptions) {
this.options = {
dataProcessingStrategy: new DefaultDataProcessingStrategy(),
validationStrategy: new DefaultValidationStrategy(),
cacheStrategy: new DefaultCacheStrategy(),
...options,
};
return ComplexPlugin;
}

async onApplicationBootstrap() {
await this.initAllStrategies();
}

async onApplicationShutdown() {
await this.destroyAllStrategies();
}

private async initAllStrategies() {
const injector = new Injector(this.moduleRef);
const strategies = [
ComplexPlugin.options.dataProcessingStrategy,
ComplexPlugin.options.validationStrategy,
ComplexPlugin.options.cacheStrategy,
];

for (const strategy of strategies) {
if (strategy && typeof strategy.init === 'function') {
await strategy.init(injector);
}
}
}

private async destroyAllStrategies() {
const strategies = [
ComplexPlugin.options.dataProcessingStrategy,
ComplexPlugin.options.validationStrategy,
ComplexPlugin.options.cacheStrategy,
];

for (const strategy of strategies) {
if (strategy && typeof strategy.destroy === 'function') {
await strategy.destroy();
}
}
}
}

Best Practices

1. Always Extend InjectableStrategy

export interface MyStrategy extends InjectableStrategy {
// ... strategy methods
}

2. Provide Sensible Defaults

Always provide a default implementation so users can use your plugin out-of-the-box:

static init(options: MyPluginOptions) {
this.options = {
myStrategy: new DefaultMyStrategy(),
...options,
};
return MyPlugin;
}

3. Handle Lifecycle Properly

Always implement proper init/destroy handling in your plugin:

async onApplicationBootstrap() {
await this.initStrategies();
}

async onApplicationShutdown() {
await this.destroyStrategies();
}

4. Use TypeScript for Better DX

Provide strong typing for better developer experience:

export interface MyStrategy extends InjectableStrategy {
processData<T>(ctx: RequestContext, data: T): Promise<ProcessedResult<T>>;
}

5. Document Your Strategy Interface

Provide comprehensive JSDoc comments:

export interface MyStrategy extends InjectableStrategy {
/**
* @description
* Processes the input data and returns a transformed result.
* This method is called for each data processing request.
*
* @param ctx - The current request context
* @param data - The input data to process
* @returns Promise resolving to the processed result
*/
processData(ctx: RequestContext, data: any): Promise<string>;
}

Summary

Custom strategies in plugins provide a powerful way to make your plugins extensible and configurable. By following the patterns outlined in this guide, you can:

  • Define clear strategy interfaces that extend InjectableStrategy
  • Provide default implementations that work out-of-the-box
  • Allow users to inject dependencies through the init() method
  • Properly manage strategy lifecycle with init() and destroy() methods
  • Enable users to provide their own implementations
  • Support configuration options for strategies

This approach ensures your plugins are flexible, maintainable, and follow Vendure's established conventions.