Skip to content

Plugins

Plugins provide a powerful way to extend HonestJS functionality by hooking into the application lifecycle. They allow you to add custom features, integrate third-party services, or modify the application's behavior without changing the core framework code.

For cross-cutting concerns like logging, authentication, validation, and request/response modification, prefer

using middleware, guards, pipes, or filters over plugins. These components are specifically designed for these use cases and provide better integration with the request lifecycle. Use plugins primarily for application-level setup, external service integration, or framework extensions.

Plugin Interface

A plugin must implement the IPlugin interface, which provides two optional lifecycle hooks:

typescript
interface IPlugin {
	logger?: ILogger

	meta?: {
		name?: string
	}

	// Runs before any modules are registered with the application.
	beforeModulesRegistered?: (app: Application, hono: Hono) => void | Promise<void>

	// Runs after all modules have been registered.
	afterModulesRegistered?: (app: Application, hono: Hono) => void | Promise<void>
}

Before either hook runs, the framework assigns logger (the optional ILogger from application options) to the plugin instance. Use this.logger?.emit(...) for structured diagnostics.

Both hooks receive:

  • app: The HonestJS Application instance. Use app.getContext() to read or write app-level pipeline data (the application context is available to your whole app, not only plugins).
  • hono: The underlying Hono application instance

Lifecycle Hooks

beforeModulesRegistered

This hook runs before any modules are registered with the application. Use this hook for:

  • Setting up global services that modules might depend on
  • Configuring the Hono instance
  • Registering global middleware that needs to run before module-specific middleware
  • Initializing external services (databases, caches, etc.)

afterModulesRegistered

This hook runs after all modules have been registered. Use this hook for:

  • Cleanup operations
  • Final configuration that requires all modules to be loaded
  • Setting up monitoring or health checks
  • Registering catch-all routes

Creating a Simple Plugin

IMPORTANT The logging example below demonstrates plugin usage, but for request logging,

middleware is the preferred approach. This example is shown for educational purposes.

Here's a basic example of a logging plugin:

typescript
import { IPlugin } from 'honestjs'
import type { Application } from 'honestjs'
import type { Hono } from 'hono'

export class LoggerPlugin implements IPlugin {
	private logLevel: string

	constructor(logLevel: string = 'info') {
		this.logLevel = logLevel
	}

	async beforeModulesRegistered(app: Application, hono: Hono): Promise<void> {
		this.logger?.emit({
			level: 'info',
			category: 'plugins',
			message: `[LoggerPlugin] Initializing with log level: ${this.logLevel}`
		})

		// Add a request logging middleware
		hono.use('*', async (c, next) => {
			const start = Date.now()
			console.log(`[${new Date().toISOString()}] ${c.req.method} ${c.req.path} - Started`)

			await next()

			const duration = Date.now() - start
			console.log(`[${new Date().toISOString()}] ${c.req.method} ${c.req.path} - ${c.res.status} (${duration}ms)`)
		})
	}

	async afterModulesRegistered(app: Application, hono: Hono): Promise<void> {
		this.logger?.emit({
			level: 'info',
			category: 'plugins',
			message: '[LoggerPlugin] All modules registered, logging is active'
		})
	}
}

Database Connection Plugin

Here's a more complex example that manages database connections:

typescript
import { IPlugin } from 'honestjs'
import type { Application } from 'honestjs'
import type { Hono } from 'hono'

interface DatabaseConfig {
	host: string
	port: number
	database: string
	username: string
	password: string
}

export class DatabasePlugin implements IPlugin {
	private config: DatabaseConfig
	private connection: any = null

	constructor(config: DatabaseConfig) {
		this.config = config
	}

	async beforeModulesRegistered(app: Application, hono: Hono): Promise<void> {
		this.logger?.emit({
			level: 'info',
			category: 'plugins',
			message: '[DatabasePlugin] Connecting to database...'
		})

		try {
			// Simulate database connection
			this.connection = await this.createConnection()

			// Make the connection available in Hono context
			hono.use('*', async (c, next) => {
				c.set('db', this.connection)
				await next()
			})

			this.logger?.emit({
				level: 'info',
				category: 'plugins',
				message: '[DatabasePlugin] Database connection established'
			})
		} catch (error) {
			this.logger?.emit({
				level: 'error',
				category: 'plugins',
				message: '[DatabasePlugin] Failed to connect to database',
				details: { error: String(error) }
			})
			throw error
		}
	}

	async afterModulesRegistered(app: Application, hono: Hono): Promise<void> {
		this.logger?.emit({
			level: 'info',
			category: 'plugins',
			message: '[DatabasePlugin] Database plugin initialization complete'
		})

		// Add a health check endpoint
		hono.get('/health/db', async (c) => {
			const isHealthy = await this.checkConnection()
			return c.json(
				{
					status: isHealthy ? 'healthy' : 'unhealthy',
					timestamp: new Date().toISOString()
				},
				isHealthy ? 200 : 503
			)
		})
	}

	private async createConnection(): Promise<any> {
		// Simulate connection creation
		return {
			host: this.config.host,
			port: this.config.port,
			connected: true
		}
	}

	private async checkConnection(): Promise<boolean> {
		// Simulate health check
		return this.connection && this.connection.connected
	}
}

Configuration and Usage

Register plugins when creating your application:

typescript
import { Application } from 'honestjs'
import { LoggerPlugin } from './plugins/logger.plugin'
import { DatabasePlugin } from './plugins/database.plugin'
import AppModule from './app.module'

const { hono } = await Application.create(AppModule, {
	plugins: [
		new LoggerPlugin('debug'),
		new DatabasePlugin({
			host: 'localhost',
			port: 5432,
			database: 'myapp',
			username: 'user',
			password: 'password'
		})
	]
})

export default hono

Plugin Types

You can provide plugins in three ways:

  1. Plugin Instance: Pass an already instantiated plugin object
typescript
plugins: [new LoggerPlugin('debug')]
  1. Plugin Class: Pass the plugin class (it will be instantiated by the framework)
typescript
plugins: [LoggerPlugin]
  1. Plugin with pre/post processors: Wrap a plugin with optional processors that run before or after the plugin's lifecycle hooks. Processors receive (app, hono, ctx) where ctx is the application context (registry). Use ctx.get / ctx.set to share pipeline data between processors and plugins.
typescript
plugins: [
	{
		plugin: new MyPlugin(),
		preProcessors: [
			async (app, hono, ctx) => {
				ctx.set('my.config', { loaded: true })
			}
		],
		postProcessors: [
			async (app, hono, ctx) => {
				const config = ctx.get<{ loaded: boolean }>('my.config')
				if (config?.loaded) {
					console.log('Plugin setup complete')
				}
			}
		]
	}
]

Execution order per plugin: preProcessorsbeforeModulesRegistered (phase 1, before modules); afterModulesRegisteredpostProcessors (phase 2, after modules).

Plugin order

Plugins run in the order they appear in options.plugins. When one plugin depends on another (for example API docs reading an artifact produced by RPC), list the producer before the consumer.

Plugin names (diagnostics)

For debug.plugins and similar logs, the framework picks a display name in this order: name on a wrapped entry (if you use { plugin, name?, ... }), then plugin.meta?.name, then the plugin class constructor name, then AnonymousPlugin#n for anonymous object plugins. Naming is for diagnostics only; it does not change execution order.

Best Practices

1. Error Handling

Always handle errors gracefully in plugins, especially in the beforeModulesRegistered hook:

typescript
async beforeModulesRegistered(app: Application, hono: Hono): Promise<void> {
	try {
		await this.initialize()
	} catch (error) {
		this.logger?.emit({
			level: 'error',
			category: 'plugins',
			message: '[MyPlugin] Initialization failed',
			details: { error: String(error) }
		})
		// Decide whether to throw the error (which fails app startup) or continue
		throw error
	}
}

2. Resource Cleanup

Consider implementing cleanup logic:

typescript
export class ResourcePlugin implements IPlugin {
	private resources: any[] = []

	async beforeModulesRegistered(app: Application, hono: Hono): Promise<void> {
		// Setup resources
		this.resources = await this.createResources()

		// Setup cleanup on process termination
		process.on('SIGTERM', () => this.cleanup())
		process.on('SIGINT', () => this.cleanup())
	}

	private async cleanup(): Promise<void> {
		console.log('[ResourcePlugin] Cleaning up resources...')
		await Promise.all(this.resources.map((resource) => resource.close()))
	}
}

3. Configuration Validation

Validate plugin configuration early:

typescript
export class ConfigurablePlugin implements IPlugin {
	constructor(private config: any) {
		this.validateConfig(config)
	}

	private validateConfig(config: any): void {
		if (!config.requiredProperty) {
			throw new Error('[ConfigurablePlugin] requiredProperty is missing from configuration')
		}
	}
}

Real-world Plugin Examples

CORS Plugin

typescript
export class CorsPlugin implements IPlugin {
	constructor(private origins: string[] = ['*']) {}

	async beforeModulesRegistered(app: Application, hono: Hono): Promise<void> {
		hono.use('*', async (c, next) => {
			c.header('Access-Control-Allow-Origin', this.origins.join(', '))
			c.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
			c.header('Access-Control-Allow-Headers', 'Content-Type, Authorization')

			if (c.req.method === 'OPTIONS') {
				return c.text('', 204)
			}

			await next()
		})
	}
}

Metrics Plugin

typescript
export class MetricsPlugin implements IPlugin {
	private requestCount = 0
	private requestDurations: number[] = []

	async beforeModulesRegistered(app: Application, hono: Hono): Promise<void> {
		hono.use('*', async (c, next) => {
			const start = Date.now()
			this.requestCount++

			await next()

			const duration = Date.now() - start
			this.requestDurations.push(duration)
		})
	}

	async afterModulesRegistered(app: Application, hono: Hono): Promise<void> {
		hono.get('/metrics', (c) => {
			const avgDuration =
				this.requestDurations.length > 0
					? this.requestDurations.reduce((a, b) => a + b, 0) / this.requestDurations.length
					: 0

			return c.json({
				totalRequests: this.requestCount,
				averageResponseTime: avgDuration,
				uptime: process.uptime()
			})
		})
	}
}

Plugins are a powerful way to keep your application modular and maintainable while adding the functionality you need.

Having plugin issues? See the FAQ for common questions about plugin ordering and naming, or the Troubleshooting page for edge cases like RPC + ApiDocs ordering pitfalls and artifact version mismatches.

Released under the MIT License.