# Patterns for Building Custom Cordis Plugins: Functions, Classes, and Dependency Injection

> Explore patterns for building custom Cordis plugins including functions, classes, and dependency injection. Learn to register and manage your plugins effectively within Fibers.

- Repository: [Cordiverse/cordis](https://github.com/cordiverse/cordis)
- Tags: how-to-guide
- Published: 2026-08-25

---

**Cordis supports three primary plugin patterns—functions, class constructors, and objects with an `apply` method—each registered via `ctx.plugin()` and executed within isolated Fibers that handle dependency injection and lifecycle management.**

Cordis is a modular, extensible framework designed for building composable applications through plugins. Understanding the patterns for building custom Cordis plugins enables developers to leverage its sophisticated dependency injection system and hierarchical loading capabilities. This guide explores the core architecture defined in `cordiverse/cordis` and provides practical implementations based on the actual source code.

## Understanding Cordis Plugin Types

### The Plugin Interface in registry.ts

In [`packages/core/src/registry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts), Cordis defines the `Plugin<T>` type as a union of three distinct patterns. Each variant extends `Plugin.Base`, which accommodates metadata including plugin names, configuration schemas, and injection requirements.

The registry recognizes these implementation patterns:

- **Plugin.Function**: `(ctx: Context, config: T) => any`
- **Plugin.Constructor**: `new (ctx: Context, config: T) => any`
- **Plugin.Object**: An object literal containing an `apply(ctx: Context, config: T): any` method

When you invoke `ctx.plugin(plugin, config)`, the `RegistryService.plugin` method inspects the provided callback, creates a **Fiber** (a lightweight execution context that holds runtime data), and returns a promise-like object that resolves when initialization completes.

## Implementing Custom Cordis Plugins

### Function-Based Plugins

The simplest pattern uses a standard function receiving the Cordis `Context` and an optional configuration object. This approach suits lightweight utilities that register event listeners or simple commands.

Define the plugin in a dedicated file:

```typescript
// src/plugins/hello.ts
import { Plugin } from 'cordis'

export const hello: Plugin.Function = (ctx, config: { greeting: string }) => {
  ctx.on('greet', () => console.log(config.greeting))
}

```

Register it through the Context:

```typescript
import { Context } from 'cordis'
import { hello } from './plugins/hello'

const ctx = new Context()
await ctx.plugin(hello, { greeting: 'Hello, Cordis!' })

```

### Class-Based Plugins with Dependency Injection

For complex plugins requiring external services, use the `@Inject` decorator defined in [`packages/core/src/registry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts). The decorator stores required service keys on the class metadata. During Fiber instantiation, `Inject.resolve` builds a dependency map and injects resolved services before invoking your plugin.

```typescript
// src/plugins/logger.ts
import { Inject } from 'cordis'

@Inject('logger')
export class LoggerPlugin {
  apply(ctx) {
    ctx.logger.info('LoggerPlugin activated')
  }
}

```

Register the instantiated class:

```typescript
await ctx.plugin(new LoggerPlugin())

```

### Object Plugins with Apply Methods

When you need a reusable, named plugin without class overhead, export an object implementing the `apply` method. This pattern offers explicit identification while maintaining the same execution signature as function plugins.

```typescript
// src/plugins/timer.ts
export const timer = {
  name: 'timer',
  apply(ctx) {
    ctx.on('tick', () => console.log('tick'))
  }
}

```

Register the object directly:

```typescript
await ctx.plugin(timer)

```

## Dynamic Configuration and Loading

### Loading Plugins from External Files

The built-in `include` plugin, implemented in [`packages/include/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/include/src/index.ts), enables declarative management through JSON or YAML files. This approach supports hot-reloading and externalized configuration without code changes.

Create a configuration file:

```yaml

# plugins.yml

- id: hello
  name: hello
  apply: ./plugins/hello.ts
  config:
    greeting: "Hi from YAML"

```

Load it through the Context:

```typescript
import { Include } from '@cordisjs/plugin-include'

await ctx.plugin(Include, { path: './plugins.yml' })

```

The `Include` plugin parses the file, builds an **EntryTree** via [`packages/loader/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/index.ts), and processes each entry through `EntryTree.update`, which ultimately calls `ctx.plugin` while preserving hierarchical loading order.

### Hot-Reloading with Patches

Cordis supports runtime configuration modifications through patches. The `applyPatches` method in [`packages/include/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/include/src/index.ts) inserts or modifies entries within the existing tree, enabling updates without application restarts.

Define a patch configuration:

```yaml

# patches.yml

patches:
  - id: hello
    insert:
      - name: extra
        apply: ./plugins/extra.ts

```

When processed by the `Include` plugin, the patch merges new entries into the target group dynamically.

## Plugin Lifecycle and Fiber Architecture

### Registration and Initialization

Every plugin registration follows a strict four-phase lifecycle defined in the core system. First, **Registration** adds the plugin to the internal registry via `ctx.plugin()`. Second, **Fiber Creation** instantiates an isolated execution context that injects declared dependencies through the `@Inject` system. Third, **Initialization** executes the plugin's body—whether function invocation, constructor execution, or `apply` method dispatch. Finally, the Fiber maintains the runtime state until disposal.

The Fiber abstraction ensures that plugins operate within isolated scopes while maintaining access to the shared Context and its services.

### Disposal and Cleanup

When removing a plugin via `registry.delete(plugin)`, Cordis automatically disposes all associated Fibers. This mechanism guarantees that event listeners, timers, and other resources allocated during initialization are properly released, preventing memory leaks in long-running applications.

## Summary

- Cordis recognizes three plugin patterns in [`packages/core/src/registry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts): functions, constructors, and objects with `apply` methods
- The `RegistryService.plugin` method creates a **Fiber** for each registered plugin to manage execution context
- The `@Inject` decorator enables declarative dependency injection by storing requirements on class metadata
- The `Include` plugin in [`packages/include/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/include/src/index.ts) supports externalized YAML/JSON configuration with hot-reloading capabilities
- The `EntryTree` in [`packages/loader/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/index.ts) processes configuration entries while preserving hierarchical loading order
- Plugin removal triggers automatic disposal of Fibers and cleanup of associated resources

## Frequently Asked Questions

### What file contains the core plugin type definitions in Cordis?

The `Plugin` type union and `Plugin.Base` interface are defined in [`packages/core/src/registry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts). This file also contains the `RegistryService` class with the `plugin` method that handles registration and the `@Inject` decorator used for dependency injection.

### How do I declare dependencies in a class-based Cordis plugin?

Apply the `@Inject` decorator to your class, passing the required service keys as arguments. The decorator stores these requirements on the class metadata, and the Fiber constructor resolves them during instantiation. For example: `@Inject('logger')` before `class MyPlugin` injects the logger service when the plugin initializes.

### Can I load Cordis plugins from external configuration files?

Yes. Use the `Include` plugin from [`packages/include/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/include/src/index.ts) to load JSON or YAML files. The plugin parses the configuration, constructs an EntryTree, and registers each entry automatically. This supports hot-reloading and allows you to manage plugin configurations without modifying source code.

### What happens when a plugin is removed from the registry?

Calling `registry.delete(plugin)` triggers the disposal phase, where Cordis destroys all Fibers associated with that plugin. This process cleans up event listeners, timers, and other resources allocated during initialization, ensuring proper memory management and preventing resource leaks.