Skip to content

Plugin Architecture

Karin is built on a modular architecture that encourages extensibility. While the core framework provides the essential building blocks (Router, DI Container, Exception Handler), sophisticated applications often require integration with external systems—Databases, Message Queues, Authentication Providers, and Monitoring Tools.

Plugins are the mechanism to bridge these external systems into the application lifecycle. Unlike simple middleware, which only operates during a request, a Plugin operates at the Application Level. It participates in the bootstrap phase, the initialization phase, and the shutdown phase.

Concept: Specialized Contractors

If the Core Framework is the General Contractor building a house:

  • Plugins are the Specialized Contractors (Electricians, Plumbers, HVAC tech).
  • Install Phase: They arrive on site, unpack their tools, and agree on the blueprints (install).
  • Init Phase: They connect the wires and turn on the water main (onPluginInit).
  • Destroy Phase: They shut down the power and clean up debris when the project ends (onPluginDestroy).

The KarinPlugin Interface

A plugin is simply a class that implements the KarinPlugin interface. This interface defines three optional lifecycle hooks.

typescript
export interface KarinPlugin {
  name: string;
  install(app: KarinApplication): void | Promise<void>;
  onPluginInit?(): void | Promise<void>;
  onPluginDestroy?(): void | Promise<void>;
}

1. install(app): The Setup Phase

This method is called synchronously (or asynchronously) immediately when KarinFactory.create() is invoked. At this stage, the HTTP server is not yet listening, and the Dependency Injection (DI) container might not be fully hydrated.

Use cases for install:

  • Registering Global Middleware: You can modify the app instance directly to add Hono/Express compatible middleware.
  • Registering Global Filters/Guards: You can enforce security policies that apply to the entire app.
  • Configuration Validation: Check if required environment variables are present before the app even tries to boot.

2. onPluginInit(): The Connection Phase

This method is called after the application has fully compiled but before it starts accepting traffic. The DI container is fully ready.

Use cases for onPluginInit:

  • Database Connections: Connect to MongoDB, Redis, or Postgres. If this fails, the application boot will halt (Fail Fast).
  • Service Registration: Register external clients (like an AWS SDK or Stripe Client) into the DI container so other services can inject them.
  • Background Tasks: Start cron jobs or message queue consumers.

3. onPluginDestroy(): The Cleanup Phase

This method is called when the application receives a termination signal (SIGTERM, SIGINT). It allows for a graceful shutdown.

Use cases for onPluginDestroy:

  • Closing Connections: Disconnect from databases to prevent hanging connections.
  • Flushing Logs: Ensure all pending telemetry is sent to the ingestion server.
  • Stopping Timers: Clear intervals to allow the process to exit cleanly.

Developing a Custom Plugin

Let's build a practical example: a Feature Flag Plugin. This plugin will fetch feature flags from an external API at startup and expose them to the rest of the application.

Step 1: Define the Plugin Class

We implement the KarinPlugin interface. We accept configuration options in the constructor to make the plugin reusable across different environments.

typescript
import { KarinPlugin, KarinApplication, Logger } from "@project-karin/core";

export class FeatureFlagPlugin implements KarinPlugin {
  name = "FeatureFlagPlugin";
  private logger = new Logger("FeatureFlags");
  private flags: Record<string, boolean> = {};

  constructor(private readonly config: { apiUrl: string; refreshInterval: number }) {}

  async install(app: KarinApplication) {
    this.logger.log("🔧 Registering Feature Flag System...");
    // We could attach a global middleware here to inject flags into every request if needed
  }

  async onPluginInit() {
    this.logger.log(`🚀 Fetching flags from ${this.config.apiUrl}...`);
    try {
      // Simulate fetching flags
      this.flags = await this.fetchFlags();
      this.logger.log(`Loaded ${Object.keys(this.flags).length} flags.`);
    } catch (error) {
      // If flags are critical, we throw here to stop the app from starting
      throw new Error(`Failed to load feature flags: ${error.message}`);
    }
  }

  private async fetchFlags() {
    // Mock implementation
    return { "new-ui": true, "beta-access": false };
  }
}

Step 2: Registering Services (Dependency Injection)

Plugins often need to expose their functionality to Controllers and Services. We can use the global container to register a provider.

typescript
import { container } from "@project-karin/core";

// Inside onPluginInit()
async onPluginInit() {
  // ... fetch flags ...
  
  // Register the flags object (or a service) as a reachable dependency
  container.registerInstance("FEATURE_FLAGS", this.flags);
}

Now, any service can inject these flags:

typescript
@Service()
export class UserService {
  constructor(@Inject("FEATURE_FLAGS") private flags: Record<string, boolean>) {}

  getDashboard() {
    if (this.flags["new-ui"]) {
      return "Modern Dashboard";
    }
    return "Legacy Dashboard";
  }
}

Step 3: Graceful Shutdown

If our plugin started a polling interval to refresh flags, we must stop it to prevent the process from hanging.

typescript
export class FeatureFlagPlugin implements KarinPlugin {
  private timer: Timer;

  async onPluginInit() {
    this.timer = setInterval(() => this.refresh(), this.config.refreshInterval);
  }

  async onPluginDestroy() {
    this.logger.log("🛑 Stopping flag refresh timer...");
    clearInterval(this.timer);
  }
}

Official vs. Ecosystem Plugins

Karin maintains a distinction between Core Plugins and Ecosystem Plugins.

  • Core Plugins: Maintained by the Karin team, guaranteed to be compatible with the current version. (e.g., @project-karin/config, @project-karin/redis).
  • Ecosystem Plugins: Community-contributed. Since plugins essentially have root access to the application lifecycle, always audit community plugins before installing them in production.

Released under the MIT License.