@expressive-tea/plugin
Build once, reuse everywhere - The plugin system that scales with your dreams
Versioning: This package uses Calendar Versioning (CalVer) in the format
YYYY.MM.MICRO(e.g.,2026.1.0). Learn more โ
Version Compatibility
This package (v2026.1.0) requires
@expressive-tea/core>= 2.0.0
- Compatible:
@expressive-tea/core@2.x.xand above- Not compatible:
@expressive-tea/core@1.x.xor belowUsing Expressive Tea Core v1.x? You'll need
@expressive-tea/plugin@1.0.3(last SemVer version for Core v1.x).Expressive Tea Core Reference: This package is designed to work with Expressive Tea Core - the main framework.
The Plugin Problem, Solved
Ever copy-pasted your database config across 10 projects?
Ever wished you could share your auth setup between apps?
Ever wanted to publish reusable middleware bundles to npm?
Say hello to the plugin system that makes code reuse actually enjoyable!
Features That'll Blow Your Mind
- True Plugin Architecture - Write once, drop into any Expressive Tea app
- Lifecycle Hooks (Boot Stages) - Control exactly when your code runs
- Smart Dependencies - Plugins can depend on other plugins (no spaghetti!)
- Priority-Based - Fine-tune execution order with simple numbers
- Decorator Magic - Just
@Stageand you're done - Battle-Tested - 93%+ coverage - we don't ship bugs
- Self-Documenting - Every API has JSDoc with examples
- Type-Safe - TypeScript strict mode = fewer runtime surprises
Installation
# npm
npm install @expressive-tea/plugin reflect-metadata
# yarn
yarn add @expressive-tea/plugin reflect-metadata
# pnpm
pnpm add @expressive-tea/plugin reflect-metadata
Requirements
- Node.js โฅ 18.0.0 (LTS recommended)
- TypeScript โฅ 5.0.0 (if you're using TypeScript)
- reflect-metadata 0.2.x
- @expressive-tea/commons 2.0.0 (auto-installed as peer dep)
TypeScript Setup
Enable decorators in your tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true, // ๐จ Required for @Stage
"emitDecoratorMetadata": true, // ๐ Required for metadata
"target": "ES2017",
"module": "commonjs"
}
}
Quick Start (60 seconds)
1. Create a Plugin
import 'reflect-metadata';
import { Plugin, BOOT_STAGES, Stage } from '@expressive-tea/plugin';
import { Express } from 'express';
export class SuperAwesomePlugin extends Plugin {
// Metadata (who are you?)
protected name = 'SuperAwesomePlugin';
protected priority = 100; // Lower = runs first
protected dependencies: string[] = []; // Other plugins you need
// Hook into BOOT_DEPENDENCIES stage
@Stage(BOOT_STAGES.BOOT_DEPENDENCIES)
async connectDatabase(server: Express): Promise<void> {
console.log('๐๏ธ Connecting to database...');
// await db.connect();
}
// Hook into APPLICATION stage
@Stage(BOOT_STAGES.APPLICATION)
setupMiddleware(server: Express): void {
console.log('๐ ๏ธ Adding middleware...');
server.use((req, res, next) => {
console.log(`${req.method} ${req.path}`);
next();
});
}
}
2. Use Your Plugin
import { Boot, ServerSettings, Pour } from '@expressive-tea/core';
import { SuperAwesomePlugin } from './plugins/awesome.plugin';
@ServerSettings({ port: 3000 })
@Pour(new SuperAwesomePlugin())
class MyApp extends Boot {}
const app = new MyApp();
app.start();
// Output:
// ๐๏ธ Connecting to database...
// ๐ ๏ธ Adding middleware...
// ๐ Server running on http://localhost:3000
BOOM! You just created a reusable plugin!
Understanding Boot Stages
Think of boot stages as lifecycle hooks - your plugin methods run at specific points during app startup:
| # | Stage | When | Perfect For |
|---|---|---|---|
| 1 | BOOT_DEPENDENCIES |
Very first | Database, Redis, external APIs |
| 2 | APPLICATION |
Express setup | Body parsers, CORS, Helmet |
| 3 | CONTROLLERS |
Route registration | REST endpoints, GraphQL |
| 4 | APPLICATION_MIDDLEWARES |
Before routes | Auth, validation, rate limiting |
| 5 | ROUTE_MIDDLEWARES |
Route-specific | Per-route guards |
| 6 | SERVER_SETTINGS |
Final setup | Port, SSL, clustering |
| 7 | AFTER_APPLICATION_MIDDLEWARES |
Post-routing | Error handlers, 404 |
Real-World Boot Stage Example
export class DatabasePlugin extends Plugin {
protected name = 'DatabasePlugin';
protected priority = 10; // Run early!
@Stage(BOOT_STAGES.BOOT_DEPENDENCIES)
async connectDB(server: Express) {
console.log('1๏ธโฃ Connecting to PostgreSQL...');
// await pool.connect();
}
@Stage(BOOT_STAGES.APPLICATION)
addDBMiddleware(server: Express) {
console.log('2๏ธโฃ Adding database middleware...');
// server.use((req, res, next) => {
// req.db = pool;
// next();
// });
}
@Stage(BOOT_STAGES.AFTER_APPLICATION_MIDDLEWARES)
addErrorHandler(server: Express) {
console.log('3๏ธโฃ Adding DB error handler...');
// server.use((err, req, res, next) => {
// if (err.code === 'ECONNREFUSED') {
// res.status(503).json({ error: 'Database unavailable' });
// }
// });
}
}
Plugin Dependencies (The Cool Part)
Plugins can depend on other plugins - the system ensures they load in the right order!
export class SessionPlugin extends Plugin {
protected name = 'SessionPlugin';
protected dependencies = ['DatabasePlugin']; // Needs DB first!
@Stage(BOOT_STAGES.APPLICATION)
setupSessions(server: Express) {
console.log('๐พ Setting up sessions (DB already connected!)');
// session logic here
}
}
export class AuthPlugin extends Plugin {
protected name = 'AuthPlugin';
protected dependencies = ['DatabasePlugin', 'SessionPlugin'];
@Stage(BOOT_STAGES.APPLICATION)
setupAuth(server: Express) {
console.log('๐ Setting up auth (DB + sessions ready!)');
// passport.js setup
}
}
// When you use them:
@Pour(new DatabasePlugin())
@Pour(new SessionPlugin())
@Pour(new AuthPlugin())
class MyApp extends Boot {}
// They'll execute in the correct order automatically!
// Output:
// 1. DatabasePlugin methods
// 2. SessionPlugin methods
// 3. AuthPlugin methods
Missing a dependency? You'll get a helpful DependencyNotFound error at startup!
Priority System (Fine-Grained Control)
Within the same boot stage, lower priority = runs first:
// Runs FIRST (priority: 10)
export class CorePlugin extends Plugin {
protected priority = 10;
}
// Runs SECOND (priority: 100)
export class MiddlewarePlugin extends Plugin {
protected priority = 100;
}
// Runs LAST (priority: 999 - the default)
export class OptionalPlugin extends Plugin {
protected priority = 999;
}
Pro tip: Use priority 1-100 for critical infrastructure, 100-500 for features, 500+ for optional stuff.
Advanced Patterns
Pattern 1: Configurable Plugins
interface DatabaseConfig {
host: string;
port: number;
database: string;
ssl: boolean;
}
export class DatabasePlugin extends Plugin {
private config: DatabaseConfig;
constructor(config: DatabaseConfig) {
super();
this.config = config;
}
@Stage(BOOT_STAGES.BOOT_DEPENDENCIES)
async connect(server: Express) {
console.log(`Connecting to ${this.config.host}:${this.config.port}...`);
// await createConnection(this.config);
}
}
// Usage with environment variables
@Pour(new DatabasePlugin({
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME || 'myapp',
ssl: process.env.DB_SSL === 'true'
}))
class MyApp extends Boot {}
Pattern 2: Multi-Stage Plugins
export class FullStackPlugin extends Plugin {
protected name = 'FullStackPlugin';
@Stage(BOOT_STAGES.BOOT_DEPENDENCIES)
loadExternalServices(server: Express) {
console.log('๐ก Loading external APIs...');
}
@Stage(BOOT_STAGES.APPLICATION)
setupCORS(server: Express) {
console.log('๐ Configuring CORS...');
// server.use(cors());
}
@Stage(BOOT_STAGES.CONTROLLERS)
registerAPIRoutes(server: Express) {
console.log('๐ฃ๏ธ Registering API routes...');
// Register your routes
}
@Stage(BOOT_STAGES.APPLICATION_MIDDLEWARES)
addAuthMiddleware(server: Express) {
console.log('๐ Adding authentication...');
// server.use(passport.authenticate());
}
@Stage(BOOT_STAGES.AFTER_APPLICATION_MIDDLEWARES)
addErrorHandlers(server: Express) {
console.log('๐จ Adding error handlers...');
// Global error handling
}
}
Pattern 3: Conditional Logic
export class MonitoringPlugin extends Plugin {
protected name = 'MonitoringPlugin';
private enableMetrics: boolean;
constructor(options: { enableMetrics?: boolean } = {}) {
super();
this.enableMetrics = options.enableMetrics ?? true;
}
@Stage(BOOT_STAGES.APPLICATION)
setupMonitoring(server: Express) {
if (this.enableMetrics) {
console.log('๐ Enabling metrics endpoint...');
// server.get('/metrics', metricsHandler);
}
if (process.env.NODE_ENV === 'production') {
console.log('๐ Enabling APM monitoring...');
// Initialize APM
}
}
}
Pattern 4: Publish to npm
// my-awesome-plugin/index.ts
import { Plugin, BOOT_STAGES, Stage } from '@expressive-tea/plugin';
import { Express } from 'express';
export class MyAwesomePlugin extends Plugin {
protected name = 'MyAwesomePlugin';
@Stage(BOOT_STAGES.APPLICATION)
setup(server: Express) {
// Your awesome logic
}
}
// Publish to npm as @yourname/awesome-plugin
// Others can use it:
// yarn add @yourname/awesome-plugin
// @Pour(new MyAwesomePlugin())
Complete API Reference
Plugin Class (Abstract)
Extend this to create your plugins:
| Property | Type | Default | What It Does |
|---|---|---|---|
name |
string |
Class name | Unique plugin identifier |
priority |
number |
999 |
Execution order (lower = first) |
dependencies |
string[] |
[] |
Required plugin names |
| Method | Returns | What It Does |
|---|---|---|
getRegisteredStage(stage) |
Array |
Get methods for a boot stage |
@Stage Decorator
@Stage(stage: BOOT_STAGES, required?: boolean)
stage- When to run this methodrequired- (Optional) Throw error if fails (default:false)
BOOT_STAGES Enum
enum BOOT_STAGES {
BOOT_DEPENDENCIES = 'Bootstrap Dependencies',
APPLICATION = 'Bootstrap Application',
CONTROLLERS = 'Bootstrap Controllers',
APPLICATION_MIDDLEWARES = 'Bootstrap Application Middlewares',
ROUTE_MIDDLEWARES = 'Bootstrap Route Middlewares',
SERVER_SETTINGS = 'Bootstrap Server Settings',
AFTER_APPLICATION_MIDDLEWARES = 'After Bootstrap Application Middlewares'
}
DependencyNotFound Exception
import { DependencyNotFound } from '@expressive-tea/plugin';
try {
// Plugin initialization
} catch (error) {
if (error instanceof DependencyNotFound) {
console.error(`Missing plugin: ${error.message}`);
}
}
Development
# Install dependencies
yarn install
# Build
yarn build
# Test
yarn test
# Watch mode
yarn test:watch
# Lint
yarn lint
# Format
yarn format
Project Structure
@expressive-tea/plugin/
โโโ src/
โ โโโ classes/
โ โ โโโ Plugin.ts # ๐ฏ Abstract Plugin class
โ โโโ decorators/
โ โ โโโ stage.ts # ๐จ @Stage decorator
โ โโโ helpers/
โ โ โโโ storage-helper.ts # ๐พ WeakMap metadata storage
โ โ โโโ object-helper.ts # ๐ ๏ธ Stage helpers
โ โโโ libs/
โ โ โโโ utilities.ts # โก Native utilities (no lodash!)
โ โโโ exceptions/
โ โ โโโ dependency.ts # โ DependencyNotFound error
โ โโโ constants.ts # ๐ BOOT_STAGES enum
โ โโโ __test__/
โ โ โโโ unit/ # ๐งช Comprehensive tests
โ โโโ index.ts # ๐ฆ Public API
โโโ dist/ # ๐๏ธ Compiled output
โโโ README.md # ๐ You are here!
Real-World Plugin Examples
Security Plugin
import helmet from 'helmet';
import cors from 'cors';
import rateLimit from 'express-rate-limit';
export class SecurityPlugin extends Plugin {
protected name = 'SecurityPlugin';
protected priority = 50;
@Stage(BOOT_STAGES.APPLICATION)
enableSecurity(server: Express) {
console.log('๐ก๏ธ Enabling security features...');
server.use(helmet());
server.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }));
server.use(rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
}));
}
}
Logging Plugin
import morgan from 'morgan';
import winston from 'winston';
export class LoggingPlugin extends Plugin {
protected name = 'LoggingPlugin';
private logger: winston.Logger;
constructor() {
super();
this.logger = winston.createLogger({
transports: [new winston.transports.Console()]
});
}
@Stage(BOOT_STAGES.APPLICATION)
setupRequestLogging(server: Express) {
console.log('๐ Setting up request logging...');
server.use(morgan('combined'));
}
@Stage(BOOT_STAGES.AFTER_APPLICATION_MIDDLEWARES)
setupErrorLogging(server: Express) {
server.use((err, req, res, next) => {
this.logger.error('Error occurred:', err);
next(err);
});
}
}
Contributing
We'd love your plugin ideas and improvements!
- Fork it
- Create your feature branch:
git checkout -b feature/epic-plugin - Add tests (we love tests!)
- Run
yarn test- green is good! - Run
yarn lint- make it pretty - Commit:
git commit -m "feat: add epic plugin feature" - Push and PR!
Check our Contributing Guide for the full details!
Related Packages
- @expressive-tea/commons - The metadata foundation
- @expressive-tea/core - The full framework
Changelog
Check CHANGELOG.md for what's cooking in each release!
Upgrading from v0.0.x?
Our Migration Guide makes it painless!
Support
Stuck? We've got your back:
- Documentation
- Gitter Chat - Real-time help
- GitHub Issues - Report bugs
- Discussions - Ask anything
- Email - Direct line
License
Apache-2.0 - Free to use, free to share, free to build amazing things! See LICENSE.
Author
Zero One IT - https://zerooneit.com
Contributors
- Diego Resendez - Original Author - @chrnx-dev
See all the awesome contributors who helped build this!
Made with and by the Expressive Tea Team
Build plugins. Share plugins. Change the world.