Plugins
Plugins in Kori are reusable pieces of functionality that extend your application's capabilities. They provide type-safe extensions, lifecycle hooks, and endpoints. This enables cross-cutting concerns like authentication, logging, and CORS.
Using Plugins
Apply plugins to your Kori application using the applyPlugin()
method. This integrates the plugin's functionality into your application:
import { createKori } from '@korix/kori';
import { corsPlugin } from '@korix/cors-plugin';
import { bodyLimitPlugin } from '@korix/body-limit-plugin';
const app = createKori()
.applyPlugin(
corsPlugin({
origin: ['https://myapp.com'],
credentials: true,
}),
)
.applyPlugin(
bodyLimitPlugin({
maxSize: 10 * 1024 * 1024, // 10MB in bytes
}),
);
How Plugins Work
A plugin is a collection of hooks and endpoints grouped together. When you apply a plugin, you're registering its hooks (like onRequest, onError) and any endpoints it defines to your application.
For example, a logging plugin might use multiple hooks:
import {
defineKoriPlugin,
createPluginLogger,
type KoriEnvironment,
type KoriRequest,
type KoriResponse,
type KoriPlugin,
} from '@korix/kori';
// Better logging plugin with dedicated logger
export function loggingPlugin<
Env extends KoriEnvironment,
Req extends KoriRequest,
Res extends KoriResponse,
>(): KoriPlugin<Env, Req, Res, unknown, { startTime: number }, unknown> {
return defineKoriPlugin({
name: 'request-logging',
apply: (kori) => {
const log = createPluginLogger({
baseLogger: kori.log(),
pluginName: 'request-logging',
});
log.info('Request logging plugin initialized');
return kori.onRequest((ctx) => {
const requestLog = createPluginLogger({
baseLogger: ctx.log(),
pluginName: 'request-logging',
});
// Log when request starts
requestLog.info('Request started', {
method: ctx.req.method(),
path: ctx.req.url().pathname,
});
// Defer response logging
ctx.defer(() => {
const duration = Date.now() - ctx.req.startTime;
requestLog.info('Request completed', {
status: ctx.res.getStatus(),
duration: `${duration}ms`,
});
});
return ctx.withReq({ startTime: Date.now() });
});
},
});
}
Plugin Order
Plugins are applied in the order they are registered. Each plugin registers its hooks to the application:
// Common order example:
const app = createKori()
// 1st: Registers hooks for CORS preflight
.applyPlugin(corsPlugin({ origin: true }))
// 2nd: Registers hooks for body size check
.applyPlugin(bodyLimitPlugin())
// 3rd: Registers hooks for security headers
.applyPlugin(securityHeadersPlugin());
Type Extensions
Plugins can add new properties to context (environment, request, response):
import { createKori } from '@korix/kori';
import { requestIdPlugin } from './my-plugins';
// Before plugin: ctx.req has base KoriRequest type
const baseApp = createKori();
// ctx.req: KoriRequest
// After plugin: ctx.req type is extended
const app = baseApp.applyPlugin(requestIdPlugin());
// ctx.req: KoriRequest & { requestId: string }
app.get('/test', {
handler: (ctx) => {
// TypeScript now knows about requestId property
const id: string = ctx.req.requestId; // ✅ Fully typed
return ctx.res.json({ requestId: id });
},
});
Chaining and Type Merging
When chaining multiple plugins with type extensions, their types are automatically merged:
✅ Good: Chained calls preserve type information
import { createKori } from '@korix/kori';
import { requestIdPlugin, timingPlugin } from './my-plugins';
const app = createKori()
// Adds { requestId: string }
.applyPlugin(requestIdPlugin())
// Adds { startTime: number }
.applyPlugin(timingPlugin());
// Final type: KoriRequest & { requestId: string } & { startTime: number }
app.get('/test', {
handler: (ctx) => {
// TypeScript knows about both extensions
const id: string = ctx.req.requestId; // ✅ From requestIdPlugin
const time: number = ctx.req.startTime; // ✅ From timingPlugin
return ctx.res.json({
requestId: id,
processingTime: Date.now() - time,
});
},
});
❌ Avoid: Separate calls lose type information
import { createKori } from '@korix/kori';
import { requestIdPlugin, timingPlugin } from './my-plugins';
const app2 = createKori();
const withRequestId = app2.applyPlugin(requestIdPlugin());
// Type extension is lost when stored in variable
const withTiming = withRequestId.applyPlugin(timingPlugin());
// TypeScript doesn't know about requestId anymore
withTiming.get('/broken', {
handler: (ctx) => {
const id = ctx.req.requestId; // ❌ TypeScript error!
},
});
Creating Custom Plugins
Define your own plugins using defineKoriPlugin()
.
TypeScript can automatically infer types from your context extensions, but explicit type definitions are useful for:
- Creating reusable plugins that other files will import
- Adding functions or methods to context objects
- Documenting your plugin's interface clearly
Basic Plugin
import {
defineKoriPlugin,
type KoriEnvironment,
type KoriRequest,
type KoriResponse,
type KoriPlugin,
} from '@korix/kori';
const timestampPlugin = <
Env extends KoriEnvironment,
Req extends KoriRequest,
Res extends KoriResponse,
>(): KoriPlugin<Env, Req, Res> =>
defineKoriPlugin({
name: 'timestamp',
apply: (kori) =>
kori.onRequest((ctx) => {
// Defer header setting until response is ready
ctx.defer(() => {
ctx.res.setHeader('x-timestamp', new Date().toISOString());
});
}),
});
Plugin with Type Extensions
Create plugins that extend context with new properties:
import {
defineKoriPlugin,
type KoriEnvironment,
type KoriRequest,
type KoriResponse,
type KoriPlugin,
} from '@korix/kori';
type RequestIdExtension = { requestId: string };
const requestIdPlugin = <
Env extends KoriEnvironment,
Req extends KoriRequest,
Res extends KoriResponse,
>(): KoriPlugin<Env, Req, Res, unknown, RequestIdExtension, unknown> =>
defineKoriPlugin({
name: 'request-id',
apply: (kori) =>
kori.onRequest((ctx) => {
const requestId = `req-${Date.now()}-${Math.random().toString(36).substring(7)}`;
// Defer header setting until response is ready
ctx.defer(() => {
ctx.res.setHeader('x-request-id', ctx.req.requestId);
});
return ctx.withReq({ requestId });
}),
});
Plugin Logging
Plugins should use dedicated loggers to provide better organization and debugging capabilities. Use createPluginLogger()
to create plugin-specific loggers that are automatically namespaced.
Plugin-Specific Loggers
import {
defineKoriPlugin,
createPluginLogger,
type KoriEnvironment,
type KoriRequest,
type KoriResponse,
type KoriPlugin,
} from '@korix/kori';
export function authPlugin<
Env extends KoriEnvironment,
Req extends KoriRequest,
Res extends KoriResponse,
>(): KoriPlugin<Env, Req, Res> {
return defineKoriPlugin({
name: 'auth',
apply: (kori) => {
// Instance-level plugin logger
const log = createPluginLogger({
baseLogger: kori.log(),
pluginName: 'auth',
});
log.info('Auth plugin initialized');
return kori.onRequest((ctx) => {
// Request-level plugin logger
const requestLog = createPluginLogger({
baseLogger: ctx.log(),
pluginName: 'auth',
});
requestLog.info('Checking authentication', {
path: ctx.req.url().pathname,
});
// Your authentication logic here...
});
},
});
}
Benefits of Plugin Loggers
Plugin-specific loggers provide several advantages:
- Namespace isolation: Logs are automatically prefixed with
plugin.{pluginName}
- Better debugging: Easy to filter logs by specific plugins
- Consistent formatting: Inherits all bindings from the base logger
- Channel separation: Plugin logs use dedicated channels for organization
Logger Output
Plugin loggers automatically namespace their output:
{
"time": 1754201824386,
"level": "info",
"channel": "plugin.auth",
"name": "request",
"message": "Checking authentication",
"meta": {
"path": "/api/users"
}
}
Compare this with regular context logging:
{
"time": 1754201824386,
"level": "info",
"channel": "app",
"name": "request",
"message": "Processing request",
"meta": {}
}
Official Plugins
Kori provides official plugins for common use cases. See the Extensions section for detailed documentation.