NextRush
Concepts

Plugins

Extend NextRush without bloating the core

Plugins extend your application without modifying the core. They add middleware, hook into request lifecycle events, and clean up after themselves on shutdown.

The Problem

Frameworks face a dilemma:

  • Include everything: Bloated core, unused code, slow cold starts
  • Include nothing: Every project reinvents common patterns

NextRush resolves this with a plugin system that keeps the core minimal. The core handles middleware composition and plugin management. Everything else — logging, events, static files, WebSocket — ships as a separate plugin package.

Mental Model

Think of plugins like USB devices for your computer:

  • The computer (core) provides ports (the Plugin interface)
  • Devices (plugins) add capabilities (keyboard, mouse, storage)
  • Each device declares what it needs (install)
  • Unplugging triggers cleanup (destroy)

The core doesn't know about specific plugins. Plugins don't modify core internals. They interact through a defined contract.

Loading diagram...

Plugin Interface

Every plugin implements this interface from @nextrush/types:

Plugin Interface
interface Plugin {
  readonly name: string;
  readonly version?: string;
  install(app: ApplicationLike): void | Promise<void>;
  destroy?(): void | Promise<void>;
}
  • name — Unique identifier. Duplicate names throw an error.
  • version — Optional semver string.
  • install — Called by app.plugin(). Receives an ApplicationLike with use() and getPlugin() methods.
  • destroy — Called during app.close(). Clean up connections, timers, and resources here.

Minimal Usage

Plugins can be plain objects, factory functions, or async with cleanup. The factory pattern is recommended for configurable plugins:

Timer Plugin
import type { Plugin } from '@nextrush/types';

const timerPlugin: Plugin = {
  name: 'timer',

  install(app) {
    app.use(async (ctx, next) => {
      const start = Date.now();
      await next();
      ctx.set('X-Response-Time', `${Date.now() - start}ms`);
    });
  },
};

app.plugin(timerPlugin);

Return a Plugin from a function to accept configuration:

Configurable Logger Plugin
import type { Plugin } from '@nextrush/types';

interface LoggerOptions {
  level: 'debug' | 'info' | 'warn' | 'error';
  prefix?: string;
}

function loggerPlugin(options: LoggerOptions): Plugin {
  return {
    name: 'logger',

    install(app) {
      app.use(async (ctx, next) => {
        console.log(`${options.prefix ?? ''} [${options.level}] → ${ctx.method} ${ctx.path}`);
        await next();
        console.log(`${options.prefix ?? ''} [${options.level}] ← ${ctx.status}`);
      });
    },
  };
}

app.plugin(loggerPlugin({ level: 'info', prefix: '[API]' }));
Database Plugin
function databasePlugin(connectionString: string): Plugin {
  let connection: DatabaseConnection | null = null;

  return {
    name: 'database',

    async install(app) {
      connection = await connect(connectionString);
      app.use(async (ctx, next) => {
        ctx.state.db = connection;
        await next();
      });
    },

    async destroy() {
      if (connection) {
        await connection.close();
        connection = null;
      }
    },
  };
}

Installing Plugins

app.plugin() handles both sync and async plugins. It returns this for sync plugins and Promise<this> for async ones.

Installing Plugins
import { createApp } from '@nextrush/core';

const app = createApp();

// Sync plugins — chainable
app.plugin(timerPlugin).plugin(eventsPlugin());

// Async plugins — await the call
await app.plugin(databasePlugin('postgres://...'));
await app.plugin(cachePlugin('redis://...'));

Do not chain after an async plugin without await. The return value is a Promise, not the app instance.

Plugin Status
// Check if installed
app.hasPlugin('logger'); // true | false

// Retrieve a plugin instance
const db = app.getPlugin<DatabasePlugin>('database');

Plugin Lifecycle

Loading diagram...

On shutdown, app.close() destroys plugins in reverse installation order using Promise.allSettled. If a plugin's destroy() throws, the error is collected but remaining plugins still run. close() returns an Error[] — empty on success.

Lifecycle Hooks

For deeper integration, implement PluginWithHooks. The application calls these hooks automatically during request processing:

Metrics Plugin with Hooks
import type { PluginWithHooks } from '@nextrush/types';

const metricsPlugin: PluginWithHooks = {
  name: 'metrics',

  install(app) {
    // Setup or register middleware
  },

  extendContext(ctx) {
    // Called before each request — add properties to ctx
  },

  onRequest(ctx) {
    ctx.state.requestStart = Date.now();
  },

  onResponse(ctx) {
    const duration = Date.now() - (ctx.state.requestStart as number);
    recordMetric('http_request_duration', duration, {
      method: ctx.method,
      path: ctx.path,
      status: ctx.status,
    });
  },

  onError(error, ctx) {
    recordMetric('http_errors', 1, {
      method: ctx.method,
      path: ctx.path,
      error: error.name,
    });
  },
};

Hook execution order per request: extendContextonRequest → middleware chain → onResponse. If the middleware chain throws, onError runs instead of onResponse.

Hook properties are validated at build time (app.callback()). If onRequest is not a function, a TypeError is thrown immediately — not silently ignored at runtime.

Error Behavior

ScenarioErrorWhen
Duplicate plugin nameError: Plugin "X" is already installedapp.plugin()
Plugin installed after app.start()Error: Cannot call plugin() after the application has startedapp.plugin()
Hook property is not a functionTypeError: Plugin "X": onRequest must be a functionapp.callback()
Plugin destroy() throwsError collected in return arrayapp.close()

Plugin dependencies are not enforced automatically. If your plugin requires another, check with app.getPlugin() inside install() and throw a descriptive error.

Checking Dependencies
function authPlugin(): Plugin {
  return {
    name: 'auth',
    install(app) {
      if (!app.getPlugin('session')) {
        throw new Error('auth plugin requires session plugin — install it first');
      }
      // ... auth middleware
    },
  };
}

Common Mistakes

Duplicate Plugin Names

// ❌ Throws: Plugin "logger" is already installed
app.plugin(loggerPlugin());
app.plugin(loggerPlugin());

// ✅ Install once
app.plugin(loggerPlugin());

Forgetting to Await Async Plugins

// ❌ Plugin not installed before next line runs
app.plugin(databasePlugin('postgres://...'));
app.plugin(authPlugin()); // database may not be ready

// ✅ Await async plugins
await app.plugin(databasePlugin('postgres://...'));
app.plugin(authPlugin());

Missing Cleanup

// ❌ Resource leak — interval never cleared
const plugin: Plugin = {
  name: 'poller',
  install() {
    setInterval(poll, 1000);
  },
};

// ✅ Store the handle, clear in destroy
function pollerPlugin(): Plugin {
  let intervalId: ReturnType<typeof setInterval> | null = null;

  return {
    name: 'poller',
    install() {
      intervalId = setInterval(poll, 1000);
    },
    destroy() {
      if (intervalId) clearInterval(intervalId);
    },
  };
}

Wrong Dependency Order

// ❌ Auth checks for session, but session not installed yet
app.plugin(authPlugin());
app.plugin(sessionPlugin());

// ✅ Install dependencies first
app.plugin(sessionPlugin());
app.plugin(authPlugin());

See Also

On this page