Modular plugin architecture for extending server functionality with dependency management and isolation in @hapi/hapi.
Register plugins to extend server functionality with proper dependency management.
/**
* Register one or more plugins
* @param plugins - Plugin object(s) or module references
* @param options - Registration options
* @returns Promise that resolves when all plugins are registered
*/
register(plugins: PluginObject | PluginObject[] | string | string[], options?: PluginRegisterOptions): Promise<void>;
interface PluginObject {
/** Plugin name */
name: string;
/** Plugin version */
version?: string;
/** Plugin registration function */
register: PluginRegister;
/** Plugin requirements */
requirements?: PluginRequirements;
/** Plugin dependencies */
dependencies?: string | string[];
/** Register plugin only once */
once?: boolean;
/** Plugin-specific options */
options?: object;
}
type PluginRegister = (server: Server, options: object) => Promise<void> | void;
interface PluginRequirements {
/** Required hapi version */
hapi?: string;
/** Required Node.js version */
node?: string;
}
interface PluginRegisterOptions {
/** Plugin options */
options?: object;
/** Registration routes prefix */
routes?: {
prefix?: string;
vhost?: string | string[];
};
/** Once registration flag */
once?: boolean;
}Usage Examples:
// Register a plugin object
const myPlugin = {
name: 'my-plugin',
version: '1.0.0',
register: async (server, options) => {
server.route({
method: 'GET',
path: '/plugin-route',
handler: () => ({ plugin: 'my-plugin', options })
});
}
};
await server.register(myPlugin, {
options: { setting: 'value' }
});
// Register multiple plugins
await server.register([
myPlugin,
{
plugin: require('@hapi/inert'),
options: { etagMethod: 'simple' }
},
{
plugin: require('@hapi/vision'),
options: {
engines: { hbs: require('handlebars') },
path: './templates'
}
}
]);
// Register plugin with route prefix
await server.register(myPlugin, {
routes: {
prefix: '/api/v1'
}
});Manage plugin dependencies and loading order.
/**
* Declare plugin dependencies
* @param dependencies - Plugin name(s) this plugin depends on
* @param after - Optional callback to run after dependencies are loaded
*/
dependency(dependencies: string | string[], after?: Function): void;Usage Examples:
const myPlugin = {
name: 'my-plugin',
dependencies: ['@hapi/inert', '@hapi/vision'],
register: async (server, options) => {
// Declare additional dependencies programmatically
server.dependency(['@hapi/good', '@hapi/bell'], () => {
console.log('All logging and authentication plugins loaded');
});
// Plugin implementation that uses dependencies
server.route({
method: 'GET',
path: '/template',
handler: {
view: { template: 'index' } // Uses @hapi/vision
}
});
}
};Expose plugin APIs for use by other plugins or the main application.
/**
* Expose plugin API
* @param key - API key name or object with multiple APIs
* @param value - API value (if key is string)
* @param options - Exposure options
*/
expose(key: string | object, value?: any, options?: ExposeOptions): void;
interface ExposeOptions {
/** Expose at server level instead of plugin level */
scope?: 'server' | 'plugin';
}Usage Examples:
const databasePlugin = {
name: 'database',
register: async (server, options) => {
const db = await connectToDatabase(options.connectionString);
// Expose database API
server.expose('db', db);
server.expose('query', (sql, params) => db.query(sql, params));
// Expose multiple APIs at once
server.expose({
users: db.collection('users'),
posts: db.collection('posts'),
comments: db.collection('comments')
});
// Server-level exposure (accessible via server.plugins.database)
server.expose('connection', db.connection, { scope: 'server' });
}
};
// Using exposed APIs in another plugin or handler
const userPlugin = {
name: 'users',
dependencies: ['database'],
register: (server, options) => {
server.route({
method: 'GET',
path: '/users',
handler: async (request, h) => {
// Access exposed database API
const db = server.plugins.database.db;
const users = await server.plugins.database.query('SELECT * FROM users');
return users;
}
});
}
};Understand plugin isolation and context through realms.
interface Realm {
/** Parent realm */
parent?: Realm;
/** Plugin name */
plugin: string;
/** Plugin options */
pluginOptions: object;
/** Registered plugins in this realm */
plugins: object;
/** Realm settings */
settings: {
/** Context binding */
bind?: object;
/** File settings */
files: {
/** Relative path base */
relativeTo?: string;
};
};
}
// Access current realm
server.realm: Realm;Usage Examples:
const pluginWithRealm = {
name: 'realm-demo',
register: (server, options) => {
console.log('Plugin realm:', server.realm.plugin);
console.log('Plugin options:', server.realm.pluginOptions);
// Set realm-specific settings
server.path(__dirname); // Set relative path for this realm
server.bind({ pluginName: 'realm-demo' }); // Bind context
server.route({
method: 'GET',
path: '/realm-info',
handler: function(request, h) {
// Access bound context
return {
plugin: this.pluginName,
realm: request.route.realm.plugin
};
}
});
}
};Register and listen to custom events within plugins.
/**
* Register custom event
* @param event - Event configuration or event name
*/
event(event: EventObject | string): void;
interface EventObject {
/** Event name */
name: string;
/** Event channels */
channels?: string[];
/** Event clone function */
clone?: Function;
/** Event spread function */
spread?: boolean;
/** Event tags */
tags?: string[];
}Usage Examples:
const eventPlugin = {
name: 'events',
register: (server, options) => {
// Register custom events
server.event('user:created');
server.event('user:updated');
server.event({
name: 'order:processed',
channels: ['email', 'sms'],
tags: ['order', 'notification']
});
// Listen to events
server.events.on('user:created', (user) => {
console.log('New user created:', user.id);
});
server.route({
method: 'POST',
path: '/users',
handler: async (request, h) => {
const user = await createUser(request.payload);
// Emit custom event
server.events.emit('user:created', user);
return h.response(user).code(201);
}
});
}
};Handle plugin lifecycle events and cleanup.
Usage Examples:
const lifecyclePlugin = {
name: 'lifecycle',
register: async (server, options) => {
const resources = await initializeResources();
// Store resources for cleanup
server.expose('resources', resources);
// Handle server stop for cleanup
server.ext('onPreStop', async () => {
console.log('Cleaning up plugin resources...');
await resources.cleanup();
});
// Plugin-specific initialization
server.log(['plugin', 'lifecycle'], 'Plugin initialized with resources');
}
};Common patterns for plugin registration and organization.
Usage Examples:
// Plugin collection pattern
const registerPlugins = async (server) => {
// Core plugins
await server.register([
{ plugin: require('@hapi/inert') },
{ plugin: require('@hapi/vision') },
{ plugin: require('@hapi/bell') }
]);
// Application plugins
await server.register([
{ plugin: require('./plugins/database') },
{ plugin: require('./plugins/auth') },
{ plugin: require('./plugins/api') }
], {
routes: { prefix: '/api' }
});
// Environment-specific plugins
if (process.env.NODE_ENV === 'development') {
await server.register([
{ plugin: require('@hapi/good') },
{ plugin: require('./plugins/dev-tools') }
]);
}
};
// Plugin factory pattern
const createDatabasePlugin = (connectionString) => ({
name: 'database',
register: async (server, options) => {
const db = await connect(connectionString);
server.expose('db', db);
}
});
// Conditional plugin registration
const registerConditionalPlugins = async (server, config) => {
const plugins = [];
if (config.enableAuth) {
plugins.push({ plugin: require('./plugins/auth') });
}
if (config.enableCaching) {
plugins.push({
plugin: require('@hapi/catbox-redis'),
options: config.redis
});
}
if (plugins.length > 0) {
await server.register(plugins);
}
};Patterns for testing plugins in isolation.
Usage Examples:
// Test plugin in isolation
const testPlugin = async () => {
const server = Hapi.server();
await server.register(myPlugin);
const response = await server.inject({
method: 'GET',
url: '/plugin-route'
});
console.log(response.statusCode); // 200
console.log(response.result); // Plugin response
};
// Test plugin dependencies
const testWithDependencies = async () => {
const server = Hapi.server();
// Register dependencies first
await server.register([
{ plugin: require('@hapi/inert') },
{ plugin: myPlugin }
]);
const exposed = server.plugins.myPlugin;
console.log('Exposed API:', exposed);
};interface Server {
/** Plugin registrations tracking */
registrations: { [pluginName: string]: PluginRegistration };
/** Exposed plugin APIs */
plugins: { [pluginName: string]: object };
/** Current plugin realm */
realm: Realm;
}
interface PluginRegistration {
/** Plugin version */
version: string;
/** Plugin name */
name: string;
/** Registration options */
options: object;
/** Plugin attributes */
attributes: object;
}
interface ExposeOptions {
/** Exposure scope */
scope?: 'server' | 'plugin';
}