Plugins & Lifecycle
Putnami applications are composed from plugins. Each plugin can hook into a lifecycle that spans build time and runtime, making it easy to extend the framework with custom functionality.
Plugin interface
A plugin is an object that implements one or more lifecycle hooks:
import type { Module, Plugin, GenerateResult } from '@putnami/application';
interface Plugin {
generate?(owner: Module): Promise<GenerateResult> | GenerateResult;
warmup?(owner: Module): Promise<void> | void;
start?(owner: Module): Promise<void> | void;
stop?(owner: Module): Promise<void> | void;
}Plugins receive their owning Module (or Application, which extends Module) as the first argument. This lets plugins resolve other plugins or access module-level configuration.
Lifecycle hooks
generate()
Runs at build time during putnami build. Use this for:
- Code generation
- Asset compilation
- Route discovery
- Type generation
import type { Module, Plugin, GenerateResult } from '@putnami/application';
import { writeFileSync } from 'fs';
export function routeTypesPlugin(): Plugin {
return {
async generate(owner: Module): Promise<GenerateResult> {
// Generate TypeScript types for routes
const routes = discoverRoutes();
const types = generateRouteTypes(routes);
writeFileSync('.gen/routes.d.ts', types);
return {
files: ['.gen/routes.d.ts'],
};
},
};
}warmup()
Runs before the application starts, after all plugins are loaded. Use this for:
- Route registration
- Cache preloading
- Connection pooling setup
- Dependency initialization
export function cachePlugin(): Plugin {
const cache = new Map<string, unknown>();
return {
async warmup(owner: Module) {
// Preload frequently accessed data
const data = await fetchInitialData();
for (const [key, value] of Object.entries(data)) {
cache.set(key, value);
}
console.log(`Cache warmed with ${cache.size} entries`);
},
};
}start()
Runs when the application starts. Use this for:
- Starting HTTP servers
- Opening database connections
- Starting background workers
- Initializing external services
export function metricsPlugin(): Plugin {
let metricsServer: Server;
return {
async start(owner: Module) {
// Start a separate metrics server
metricsServer = Bun.serve({
port: 9090,
fetch(req) {
return new Response(getMetrics(), {
headers: { 'Content-Type': 'text/plain' },
});
},
});
console.log('Metrics server started on port 9090');
},
async stop() {
metricsServer?.stop();
},
};
}stop()
Runs during graceful shutdown (in reverse order of plugin registration). Use this for:
- Closing connections
- Flushing buffers
- Cleanup operations
- Releasing resources
export function databasePlugin(): Plugin {
let connection: DatabaseConnection;
return {
async start() {
connection = await createConnection();
},
async stop() {
// Gracefully close the database connection
await connection?.close();
console.log('Database connection closed');
},
};
}Creating custom plugins
Basic plugin
import type { Plugin } from '@putnami/application';
export function myPlugin(): Plugin {
return {
async warmup() {
console.log('My plugin is warming up');
},
async start() {
console.log('My plugin started');
},
async stop() {
console.log('My plugin stopped');
},
};
}
// Usage
const app = application()
.use(myPlugin());Plugin with configuration
import type { Plugin } from '@putnami/application';
interface LoggerPluginOptions {
level: 'debug' | 'info' | 'warn' | 'error';
prefix?: string;
}
export function loggerPlugin(options: LoggerPluginOptions): Plugin {
const { level, prefix = '[APP]' } = options;
return {
async warmup() {
console.log(`${prefix} Logger initialized at ${level} level`);
},
};
}
// Usage
const app = application()
.use(loggerPlugin({ level: 'info', prefix: '[MyApp]' }));Plugin with state
import type { Plugin } from '@putnami/application';
import { json } from '@putnami/application';
export function rateLimiterPlugin(maxRequests: number, windowMs: number): Plugin {
const requests = new Map<string, number[]>();
const isRateLimited = (ip: string): boolean => {
const now = Date.now();
const windowStart = now - windowMs;
const timestamps = requests.get(ip) || [];
const recent = timestamps.filter(t => t > windowStart);
if (recent.length >= maxRequests) {
return true;
}
recent.push(now);
requests.set(ip, recent);
return false;
};
return {
async warmup(owner: Module) {
// Access other plugins if needed
const httpPlugin = owner.getPlugin(HttpPlugin);
httpPlugin.prepend(async (ctx, next) => {
const ip = ctx.headers.get('x-forwarded-for') || 'unknown';
if (isRateLimited(ip)) {
return json(
{ error: 'Too many requests' },
{ status: 429 }
);
}
return next();
});
},
};
}Plugin class
For complex plugins, use a class:
import type { Module, Plugin } from '@putnami/application';
import { json } from '@putnami/application';
export class HealthCheckPlugin implements Plugin {
private checks: Map<string, () => Promise<boolean>> = new Map();
addCheck(name: string, check: () => Promise<boolean>) {
this.checks.set(name, check);
}
async warmup(owner: Module) {
const httpPlugin = owner.getPlugin(HttpPlugin);
httpPlugin.get('/health', async () => {
const results: Record<string, boolean> = {};
for (const [name, check] of this.checks) {
try {
results[name] = await check();
} catch {
results[name] = false;
}
}
const healthy = Object.values(results).every(Boolean);
return json(
{ healthy, checks: results },
{ status: healthy ? 200 : 503 }
);
});
}
}
// Usage
const healthCheck = new HealthCheckPlugin();
healthCheck.addCheck('database', async () => {
return await database().query('SELECT 1');
});
const app = application()
.use(healthCheck);Accessing other plugins
Get a registered plugin
import { HttpPlugin } from '@putnami/application';
export function myPlugin(): Plugin {
return {
async warmup(owner: Module) {
// Get an existing plugin (throws if not found)
const httpPlugin = owner.getPlugin(HttpPlugin);
// Register routes
httpPlugin.get('/my-route', () => ({ ok: true }));
},
};
}Ensure a plugin exists
export function myPlugin(): Plugin {
return {
async warmup(owner: Module) {
// Get or wait for a plugin to be ready
const httpPlugin = await owner.ensurePlugin(HttpPlugin);
httpPlugin.get('/my-route', () => ({ ok: true }));
},
};
}List all plugins
export function debugPlugin(): Plugin {
return {
async warmup(owner: Module) {
const plugins = owner.getPlugins();
console.log(`Loaded ${plugins.length} plugins`);
},
};
}Module composition
Modules let you group related plugins into reusable, composable units. The hierarchy is Application > Module > Plugin: applications and modules can contain plugins and sub-modules, but only an Application has a startable lifecycle.
Creating a module
import { module } from '@putnami/application';
const authModule = module('auth')
.path('/auth')
.use(oAuth2())
.use(api());Module path
Use .path() to set a base path for the module. Child plugins (api(), react(), statics()) automatically inherit this path as their route prefix:
import { module, api } from '@putnami/application';
import { react } from '@putnami/react';
const tasksModule = module('tasks')
.path('/tasks') // All child routes prefixed with /tasks
.use(api()) // API routes: /tasks, /tasks/[id], etc.
.use(react()); // React pages: /tasks, /tasks/new, etc.If a plugin specifies an explicit prefix, it takes priority over the module path.
Module security
Use .secure() to protect all routes registered by plugins in the module:
const dashboard = module('dashboard')
.path('/dashboard')
.secure() // Require authentication
.use(api());
const admin = module('admin')
.path('/admin')
.secure({ roles: ['admin'] }) // Require admin role
.use(api());Composing modules
import { application, http } from '@putnami/application';
const app = application()
.use(http({ port: 3000 }))
.use(tasksModule)
.use(authModule);
await app.start();When the application starts, plugins from all modules are collected depth-first and executed in registration order. During shutdown, plugins are stopped in reverse order.
Nested modules
Modules can contain other modules:
const inner = module('inner').use(somePlugin());
const outer = module('outer').use(inner).use(anotherPlugin());
const app = application().use(outer);Plugin resolution across modules
Plugins can find other plugins by walking up the module hierarchy. This means a plugin registered on a parent module is visible to all child modules:
export function myPlugin(): Plugin {
return {
async warmup(owner) {
// Searches owner's plugins first, then walks up to parent modules
const http = owner.getPlugin(HttpPlugin);
http.get('/my-route', () => ({ ok: true }));
},
};
}Modules with DI providers
Modules can declare DI providers and requirements:
const dbModule = module('db')
.provide(DatabaseService)
.use(sql());
const authModule = module('auth')
.require(DatabaseService)
.provide(AuthService, { deps: [DatabaseService] })
.use(oAuth2());Application lifecycle
Creating an application
Use the application() factory for fluent composition, or instantiate new Application() directly when needed:
import { application } from '@putnami/application';
const app = application()
.use(http({ port: 3000 }))
.use(myPlugin());Running an application
const app = application()
.use(http({ port: 3000 }))
.use(myPlugin());
// Start the application
await app.start();
// Check if running
console.log(app.isRunning()); // true
// Stop the application
await app.stop();One-shot jobs
For scripts that run once and exit:
const job = application()
.use(sql())
.run(async () => {
// This runs after warmup, before start
await migrateDatabase();
console.log('Migration complete');
});
await job.start();
// Application exits after the run callback completesShutdown hooks
const app = application()
.use(http())
.onStop(async () => {
// Custom shutdown logic
await flushLogs();
await closeConnections();
});Plugin ordering
Plugins are executed in registration order for generate, warmup, and start. For stop, they execute in reverse order:
const app = application()
.use(pluginA()) // warmup: 1st, stop: 3rd
.use(pluginB()) // warmup: 2nd, stop: 2nd
.use(pluginC()); // warmup: 3rd, stop: 1stBuild vs runtime
Build-only plugins
Some plugins only run at build time:
export function typeGenPlugin(): Plugin {
return {
async generate(app) {
// Only runs during `putnami build`
await generateTypes();
return { files: ['.gen/types.ts'] };
},
// No runtime hooks
};
}Runtime-only plugins
Some plugins only run at runtime:
export function requestLoggerPlugin(): Plugin {
return {
async warmup(owner) {
const http = owner.getPlugin(HttpPlugin);
http.use(async (ctx, next) => {
const start = Date.now();
const response = await next();
console.log(`${ctx.method} ${ctx.path()} - ${Date.now() - start}ms`);
return response;
});
},
};
}