Plugins

Plugins are how you extend Ingenium - typed decorators on ctx, lifecycle hooks, and self-contained registration via app.register.

Writing a plugin

import { ingenium, type IngeniumPlugin } from 'ingenium'

interface User { id: string; email: string }

const auth: IngeniumPlugin<{ secret: string }> = (app, opts) => {
  app.decorate('user', async (ctx) => {
    const token = ctx.headers.authorization?.split(' ')[1]
    if (!token) throw new IngeniumUnauthorizedError()
    return verifyToken(token, opts.secret) as User
  })
  app.hooks.onRequest((ctx) => {
    ctx.state.requestId = crypto.randomUUID()
  })
}

const app = ingenium()
await app.register(auth, { secret: process.env.JWT_SECRET! })

declare module 'ingenium' {
  interface IngeniumContext {
    user: User
  }
}

app.get('/me', (ctx) => ctx.user)  // typed, lazily resolved on first access

Lifecycle hooks

onRoute, onCompose, onRequest, onResponse, onError. Hooks let you observe or mutate behavior at the right point in the lifecycle without having to write a wrapping middleware.

Decorators

Decorators come in two flavors:

  • Lazy (decorate) - defineProperty self-replacing getter, computed on first access. The decorator function runs only if the property is read during the request.
  • Eager (decorateRequest) - assigned at request start. Slightly more expensive per request, but the value is always there.

The hot path checks hooks.hasAny() and decorators.hasAny() so plugin-free apps pay zero overhead.

Module augmentation

To get ctx.user typed across your codebase, declare the augmentation alongside the plugin:

declare module 'ingenium' {
  interface IngeniumContext {
    user: User
  }
}

Put it in a .d.ts file (or alongside the plugin) and TypeScript will pick it up project-wide.

Where to next?