Skip to content

Plugin System

Extend core functionality with self-contained plugins.

Overview

The plugin system allows feature packages to register providers, exports, and controllers through a unified interface. Plugins are registered via RedisModule.forRoot() and managed by PluginRegistryService.

IRedisXPlugin Interface

typescript
interface IRedisXPlugin {
  /** Unique plugin identifier (lowercase, alphanumeric with hyphens) */
  readonly name: string;

  /** Plugin version following semver */
  readonly version: string;

  /** Optional human-readable description */
  readonly description?: string;

  /** Names of plugins this plugin depends on */
  readonly dependencies?: string[];

  /** Called when plugin is registered (sync setup) */
  onRegister?(context: IPluginContext): void | Promise<void>;

  /** Called after all plugins registered and Redis connected */
  onModuleInit?(context: IPluginContext): void | Promise<void>;

  /** Called on shutdown (cleanup, reverse dependency order) */
  onModuleDestroy?(context: IPluginContext): void | Promise<void>;

  /** Returns NestJS providers this plugin contributes */
  getProviders?(): Provider[];

  /** Returns exports that other modules can inject */
  getExports?(): Array<Provider | string | symbol | Type>;

  /** Returns NestJS controllers this plugin contributes */
  getControllers?(): Type[];
}

Registering Plugins

Synchronous

typescript
import { Module } from '@nestjs/common';
import { RedisModule } from '@nestjs-redisx/core';
import { CachePlugin } from '@nestjs-redisx/cache';
import { LocksPlugin } from '@nestjs-redisx/locks';

@Module({
  imports: [
    RedisModule.forRoot({
      clients: { host: 'localhost', port: 6379 },
      plugins: [
        new CachePlugin({ l2: { defaultTtl: 3600 } }),
        new LocksPlugin({ defaultTtl: 30000 }),
      ],
    }),
  ],
})
export class AppModule {}

Asynchronous

Plugins are provided outside useFactory — they must be available at module construction time:

typescript
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { RedisModule } from '@nestjs-redisx/core';
import { CachePlugin } from '@nestjs-redisx/cache';
import { LocksPlugin } from '@nestjs-redisx/locks';

@Module({
  imports: [
    RedisModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      plugins: [
        new CachePlugin({ l2: { defaultTtl: 3600 } }),
        new LocksPlugin({ defaultTtl: 30000 }),
      ],
      useFactory: (config: ConfigService) => ({
        clients: {
          host: config.get<string>('REDIS_HOST', 'localhost'),
          port: config.get<number>('REDIS_PORT', 6379),
        },
      }),
    }),
  ],
})
export class AppModule {}

Lifecycle Hooks

Hooks are managed by PluginRegistryService and called in dependency order.

Execution Order

1. onRegister()      — all plugins, in dependency order
2. onModuleInit()    — all plugins, in dependency order
3. (runtime)
4. onModuleDestroy() — all plugins, in REVERSE dependency order

All hooks are optional. Plugins without hooks are silently skipped.

onRegister

Called immediately during module initialization. Use for synchronous setup.

typescript
onRegister(context: IPluginContext): void {
  context.logger.info('Plugin registered');
}

onModuleInit

Called after all plugins are registered. Use for async setup like loading Lua scripts.

typescript
async onModuleInit(context: IPluginContext): Promise<void> {
  // Dependencies are guaranteed to be initialized
  const cachePlugin = context.getPlugin('cache');
  context.logger.info('Plugin initialized');
}

onModuleDestroy

Called during shutdown in reverse dependency order. Dependencies are still alive when your plugin shuts down.

typescript
async onModuleDestroy(context: IPluginContext): Promise<void> {
  // Flush buffers, close connections
  context.logger.info('Plugin destroyed');
}

Plugin Dependencies

Declare dependencies with the dependencies array. The system uses topological sort (Kahn's algorithm) to determine initialization order.

typescript
import { IRedisXPlugin, IPluginContext } from '@nestjs-redisx/core';

export class AuditPlugin implements IRedisXPlugin {
  readonly name = 'audit';
  readonly version = '1.0.0';
  readonly dependencies = ['cache']; // Depends on cache plugin

  async onModuleInit(context: IPluginContext) {
    // CachePlugin.onModuleInit() is guaranteed to have run already
    const cache = context.getPlugin('cache');
  }

  async onModuleDestroy(context: IPluginContext) {
    // CachePlugin is still alive here
    // AuditPlugin shuts down BEFORE CachePlugin
  }
}

Dependency Errors

Missing dependency:

typescript
plugins: [new AuditPlugin()] // cache not registered!
// Throws: Plugin "audit" depends on "cache" which is not registered

Circular dependency:

typescript
// A depends on B, B depends on A
// Throws: Circular dependency detected among plugins: audit, cache

IPluginContext

Context provided to all lifecycle hooks.

typescript
interface IPluginContext {
  /** Client manager for accessing and querying Redis clients */
  readonly clientManager: IClientManager;

  /** Module configuration (global settings, plugins list) */
  readonly config: IRedisXConfig;

  /** Scoped logger instance */
  readonly logger: IRedisXLogger;

  /** NestJS ModuleRef for advanced DI operations */
  readonly moduleRef: ModuleRef;

  /** Gets another plugin by name */
  getPlugin<T extends IRedisXPlugin>(name: string): T | undefined;

  /** Checks if a plugin is loaded */
  hasPlugin(name: string): boolean;
}

interface IClientManager {
  /** Gets Redis client by name (async) */
  getClient(name?: string): Promise<IRedisDriver>;

  /** Checks if client exists */
  hasClient(name: string): boolean;

  /** Gets all registered client names */
  getClientNames(): string[];
}

Context Usage

typescript
import { Injectable } from '@nestjs/common';
import { IRedisXPlugin, IPluginContext } from '@nestjs-redisx/core';

@Injectable()
class SomeService {}

export class ContextDemoPlugin implements IRedisXPlugin {
  readonly name = 'context-demo';
  readonly version = '1.0.0';

  async onModuleInit(context: IPluginContext) {
    // Check if another plugin is available
    if (context.hasPlugin('metrics')) {
      const metrics = context.getPlugin('metrics');
      context.logger.info('Metrics plugin detected');
    }

    // Access global config
    const prefix = context.config.global?.keyPrefix;

    // Check client existence and get client
    if (context.clientManager.hasClient('cache')) {
      const client = await context.clientManager.getClient('cache');
    }

    // List all registered clients
    const clientNames = context.clientManager.getClientNames();

    // Get the default client
    const defaultClient = await context.clientManager.getClient();

    // Use NestJS DI for advanced cases
    const someService = context.moduleRef.get(SomeService);
  }
}

Creating a Plugin

Minimal Plugin

typescript
import { Injectable, Provider } from '@nestjs/common';
import { IRedisXPlugin } from '@nestjs-redisx/core';

@Injectable()
class MyService {}

export class MyPlugin implements IRedisXPlugin {
  readonly name = 'my-plugin';
  readonly version = '1.0.0';

  getProviders(): Provider[] {
    return [{ provide: 'MY_SERVICE', useClass: MyService }];
  }

  getExports(): Array<string | symbol> {
    return ['MY_SERVICE'];
  }
}

Full Plugin with Lifecycle

typescript
import { Injectable, Provider } from '@nestjs/common';
import { IRedisXPlugin, IPluginContext } from '@nestjs-redisx/core';

interface MyPluginOptions {
  defaultTimeout?: number;
}

const MY_OPTIONS = Symbol('MY_OPTIONS');
const MY_SERVICE = Symbol('MY_SERVICE');

@Injectable()
class MyPluginService {}

export class MyPlugin implements IRedisXPlugin {
  readonly name = 'my-plugin';
  readonly version = '1.0.0';
  readonly description = 'Example custom plugin';

  constructor(private readonly options: MyPluginOptions = {}) {}

  getProviders(): Provider[] {
    return [
      { provide: MY_OPTIONS, useValue: this.options },
      { provide: MY_SERVICE, useClass: MyPluginService },
    ];
  }

  getExports(): Array<string | symbol | Provider> {
    return [MY_SERVICE];
  }

  async onRegister(context: IPluginContext): Promise<void> {
    context.logger.info('Plugin registered');
  }

  async onModuleInit(context: IPluginContext): Promise<void> {
    context.logger.info('Plugin initialized');
  }

  async onModuleDestroy(context: IPluginContext): Promise<void> {
    context.logger.info('Plugin destroyed');
  }
}

Available Plugins

PackagePluginDescription
@nestjs-redisx/cacheCachePluginL1+L2 caching, SWR, tag invalidation
@nestjs-redisx/locksLocksPluginDistributed locks with auto-renewal
@nestjs-redisx/rate-limitRateLimitPluginToken bucket, sliding window, fixed window
@nestjs-redisx/idempotencyIdempotencyPluginIdempotent request handling
@nestjs-redisx/streamsStreamsPluginRedis Streams consumer/producer
@nestjs-redisx/metricsMetricsPluginPrometheus metrics
@nestjs-redisx/tracingTracingPluginOpenTelemetry tracing

PluginRegistryService

The internal service that manages plugin lifecycle. Exported for advanced use cases.

typescript
import { PluginRegistryService, REGISTERED_PLUGINS } from '@nestjs-redisx/core';
TokenTypeDescription
REGISTERED_PLUGINSIRedisXPlugin[]Array of all registered plugins
PluginRegistryService@Injectable()Manages lifecycle hooks and dependency ordering

Naming Conventions

TypePatternExample
Plugin class{Feature}PluginCachePlugin, LocksPlugin
Service token{FEATURE}_SERVICELOCK_SERVICE, CACHE_SERVICE
Options token{FEATURE}_PLUGIN_OPTIONSLOCKS_PLUGIN_OPTIONS
Store token{FEATURE}_STORELOCK_STORE

Next Steps

Released under the MIT License.