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
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
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:
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 orderAll hooks are optional. Plugins without hooks are silently skipped.
onRegister
Called immediately during module initialization. Use for synchronous setup.
onRegister(context: IPluginContext): void {
context.logger.info('Plugin registered');
}onModuleInit
Called after all plugins are registered. Use for async setup like loading Lua scripts.
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.
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.
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:
plugins: [new AuditPlugin()] // cache not registered!
// Throws: Plugin "audit" depends on "cache" which is not registeredCircular dependency:
// A depends on B, B depends on A
// Throws: Circular dependency detected among plugins: audit, cacheIPluginContext
Context provided to all lifecycle hooks.
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
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
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
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
| Package | Plugin | Description |
|---|---|---|
@nestjs-redisx/cache | CachePlugin | L1+L2 caching, SWR, tag invalidation |
@nestjs-redisx/locks | LocksPlugin | Distributed locks with auto-renewal |
@nestjs-redisx/rate-limit | RateLimitPlugin | Token bucket, sliding window, fixed window |
@nestjs-redisx/idempotency | IdempotencyPlugin | Idempotent request handling |
@nestjs-redisx/streams | StreamsPlugin | Redis Streams consumer/producer |
@nestjs-redisx/metrics | MetricsPlugin | Prometheus metrics |
@nestjs-redisx/tracing | TracingPlugin | OpenTelemetry tracing |
PluginRegistryService
The internal service that manages plugin lifecycle. Exported for advanced use cases.
import { PluginRegistryService, REGISTERED_PLUGINS } from '@nestjs-redisx/core';| Token | Type | Description |
|---|---|---|
REGISTERED_PLUGINS | IRedisXPlugin[] | Array of all registered plugins |
PluginRegistryService | @Injectable() | Manages lifecycle hooks and dependency ordering |
Naming Conventions
| Type | Pattern | Example |
|---|---|---|
| Plugin class | {Feature}Plugin | CachePlugin, LocksPlugin |
| Service token | {FEATURE}_SERVICE | LOCK_SERVICE, CACHE_SERVICE |
| Options token | {FEATURE}_PLUGIN_OPTIONS | LOCKS_PLUGIN_OPTIONS |
| Store token | {FEATURE}_STORE | LOCK_STORE |
Next Steps
- Configuration — Module configuration with plugins
- Error Handling — Error system reference
- Troubleshooting — Common issues