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) -definePropertyself-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?
- IngeniumContext - the surface you're decorating.
- Core concepts -
app.registeris part of the App API. - Errors - what to throw from decorator resolvers.
- Middleware - when a middleware is the simpler choice.