Putnami
DocsGitHub

Licensed under FSL-1.1-MIT

Getting Started
Concepts
How To
Build A Web App
Build An Api Service
Share Code Between Projects
Configure Your App
Add Persistence
Add Authentication
Add Background Jobs
Develop With Ai
Structure Business Logic With Di
Upgrade Putnami
Principles
Tooling & Workspace
Workspace
Cli
Jobs & Caching
Extensions
Templates
Error Handling
Frameworks
Typescript
ExtensionOverviewWebReact RoutingForms And ActionsStatic FilesApiErrors And ResponsesConfigurationLoggingHttp And MiddlewareDependency InjectionPlugins And LifecycleSessionsAuthPersistenceEventsStorageCachingWebsocketsTestingHealth ChecksTelemetryProto GrpcSmart ClientSchema
Go
ExtensionOverviewHttpDependency InjectionPlugins And LifecycleConfigurationSecurityPersistenceErrorsEventsStorageCachingLoggingTelemetryGrpcService ClientsValidationOpenapiTesting
Python
Extension
Platform
Ci
  1. DocsSeparator
  2. FrameworksSeparator
  3. TypescriptSeparator
  4. Plugins And Lifecycle

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/web';

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 completes

Shutdown 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: 1st

Build 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;
      });
    },
  };
}

Related guides

  • Overview
  • HTTP & Middleware

On this page

  • Plugins & Lifecycle
  • Plugin interface
  • Lifecycle hooks
  • generate()
  • warmup()
  • start()
  • stop()
  • Creating custom plugins
  • Basic plugin
  • Plugin with configuration
  • Plugin with state
  • Plugin class
  • Accessing other plugins
  • Get a registered plugin
  • Ensure a plugin exists
  • List all plugins
  • Module composition
  • Creating a module
  • Module path
  • Module security
  • Composing modules
  • Nested modules
  • Plugin resolution across modules
  • Modules with DI providers
  • Application lifecycle
  • Creating an application
  • Running an application
  • One-shot jobs
  • Shutdown hooks
  • Plugin ordering
  • Build vs runtime
  • Build-only plugins
  • Runtime-only plugins
  • Related guides