Middleware
Middleware lets you run code before and after route handlers. Kyrin supports three patterns: middleware functions, hooks, and plugins.
Middleware Functions
Middleware wraps around your handlers in an "onion" model:
app.use(async (c, next) => {
// Before handler
console.log("Request started");
await next(); // Call the handler
// After handler
console.log("Request finished");
});The execution order looks like this:
Request → MW1 before → MW2 before → Handler → MW2 after → MW1 after → ResponseAdding Middleware
Use app.use() to register global middleware:
const app = new Kyrin();
app.use(async (c, next) => {
const start = Date.now();
await next();
console.log(`${c.method} ${c.path} - ${Date.now() - start}ms`);
});
app.get("/", () => "Hello");Middleware runs in the order you register it.
Early Response
Return a Response from middleware to short-circuit the chain:
app.use(async (c, next) => {
const auth = c.header("Authorization");
if (!auth) {
return new Response("Unauthorized", { status: 401 });
}
await next();
});When you return a Response, the handler (and remaining middleware) won't run.
Modifying Context
Use c.store to pass data between middleware and handlers:
app.use(async (c, next) => {
c.store.requestId = crypto.randomUUID();
c.store.user = await getUserFromToken(c.header("Authorization"));
await next();
});
app.get("/profile", (c) => {
return { user: c.store.user, requestId: c.store.requestId };
});Modifying Response Headers
Set headers that will be included in the response:
app.use(async (c, next) => {
c.set.headers["X-Request-Id"] = crypto.randomUUID();
c.set.headers["X-Powered-By"] = "Kyrin";
await next();
});Hooks
Hooks are simpler than middleware — they run at specific points and don't need to call next().
onRequest
Runs before the handler:
app.onRequest((c) => {
c.store.startTime = Date.now();
console.log(`→ ${c.method} ${c.path}`);
});Return a Response to stop processing:
app.onRequest((c) => {
if (c.path.startsWith("/admin") && !isAdmin(c)) {
return new Response("Forbidden", { status: 403 });
}
});onResponse
Runs after the handler:
app.onResponse((c) => {
const duration = Date.now() - c.store.startTime;
console.log(`← ${c.method} ${c.path} (${duration}ms)`);
});When to Use Hooks vs Middleware
- Hooks: Simple tasks like logging, timing, or early rejection
- Middleware: Complex logic that needs to wrap the handler (transactions, error handling)
// Hooks — simple and clean
app.onRequest((c) => {
c.store.start = Date.now();
});
app.onResponse((c) => {
console.log(`${Date.now() - c.store.start}ms`);
});
// Middleware — when you need try/catch around the handler
app.use(async (c, next) => {
try {
await next();
} catch (err) {
console.error(err);
return c.json({ error: "Internal error" }, 500);
}
});Guard
Use guard() to protect a group of routes with middleware:
const auth = async (c, next) => {
const token = c.header("Authorization")?.replace("Bearer ", "");
if (!token) {
return c.json({ error: "No token" }, 401);
}
const user = await verifyToken(token);
if (!user) {
return c.json({ error: "Invalid token" }, 401);
}
c.store.user = user;
await next();
};
// These routes require authentication
app.guard(auth, (app) => {
app.get("/profile", (c) => c.store.user);
app.get("/settings", (c) => ({ user: c.store.user }));
app.put("/settings", async (c) => {
const body = await c.body();
return { updated: true };
});
});
// These don't
app.get("/", () => "Public");
app.post("/login", () => ({ token: "..." }));Plugins
Plugins bundle middleware, hooks, and configuration together.
Using Plugins
import { Kyrin, cors } from "kyrin";
const app = new Kyrin();
app.use(cors()); // Add CORS pluginPlugin Structure
A plugin is an object with optional middleware, onRequest, and onResponse:
const myPlugin = {
name: "my-plugin",
middleware: async (c, next) => {
// Runs like normal middleware
await next();
},
onRequest: (c) => {
// Runs before handler
},
onResponse: (c) => {
// Runs after handler
},
};
app.use(myPlugin);Creating Plugins
Use a factory function for configurable plugins:
interface RateLimitOptions {
max: number;
windowMs: number;
}
const rateLimit = (options: RateLimitOptions) => {
const requests = new Map<string, number[]>();
return {
name: "rate-limit",
middleware: async (c, next) => {
const ip = c.header("X-Forwarded-For") || "unknown";
const now = Date.now();
const windowStart = now - options.windowMs;
// Get requests in current window
const timestamps = requests.get(ip) || [];
const recent = timestamps.filter((t) => t > windowStart);
if (recent.length >= options.max) {
return c.json({ error: "Too many requests" }, 429);
}
recent.push(now);
requests.set(ip, recent);
await next();
},
};
};
// Use it
app.use(rateLimit({ max: 100, windowMs: 60000 }));CORS Plugin
Cross-Origin Resource Sharing (CORS) is handled by the built-in cors plugin.
Basic Usage
Allow all origins:
import { Kyrin, cors } from "kyrin";
const app = new Kyrin();
app.use(cors());Configuration Options
app.use(
cors({
origin: "*", // Allow all origins (default)
methods: ["GET", "POST", "PUT", "DELETE"], // Allowed methods
allowedHeaders: ["Content-Type", "Authorization"], // Allowed headers
credentials: false, // Allow credentials
maxAge: 86400, // Preflight cache (seconds)
})
);Specific Origins
Allow a single origin:
app.use(
cors({
origin: "https://example.com",
})
);Allow multiple origins:
app.use(
cors({
origin: ["https://example.com", "https://admin.example.com"],
})
);Dynamic origin validation:
app.use(
cors({
origin: (origin) => {
return origin.endsWith(".example.com");
},
})
);With Credentials
When your frontend needs to send cookies:
app.use(
cors({
origin: "https://example.com", // Must be specific, not "*"
credentials: true,
})
);Preflight Requests
CORS preflight (OPTIONS) requests are handled automatically. The response includes:
Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-HeadersAccess-Control-Max-AgeAccess-Control-Allow-Credentials(if enabled)
Logger Plugin
NestJS-style logging middleware for Kyrin framework.
bun add @kyrinjs/loggerBasic Usage
import { Kyrin } from "kyrin";
import { logger } from "@kyrinjs/logger";
const app = new Kyrin();
app.use(logger());
app.get("/", () => "Hello");
app.listen(3000);Output:
[Kyrin] 2025-12-15 01:25:33 LOG [HTTP] GET / 200 +2msConfiguration
app.use(
logger({
enabled: true, // Enable/disable logging
timestamp: true, // Show timestamp
colorize: true, // Enable ANSI colors
context: "HTTP", // Context name in brackets
level: "log", // Minimum log level
skip: ["/health"], // Paths to skip
logQuery: true, // Show query parameters
})
);Skipping Paths
// Array of paths
app.use(logger({ skip: ["/health", "/metrics"] }));
// Or function
app.use(logger({ skip: (path) => path.startsWith("/internal") }));Log Levels
From lowest to highest: verbose → debug → log → warn → error
// Only show warnings and errors
app.use(logger({ level: "warn" }));Status Code Colors
| Range | Color | Meaning |
|---|---|---|
| 2xx | Green | Success |
| 3xx | Cyan | Redirect |
| 4xx | Yellow | Client Error |
| 5xx | Red | Server Error |
Custom Format
app.use(
logger({
format: (data) => `${data.method} ${data.path} - ${data.statusCode}`,
})
);Available data: method, path, statusCode, duration, timestamp, context
Color Utilities
import { colorMethod, colorStatus, formatDuration } from "@kyrinjs/logger";
app.use(
logger({
format: (data) => {
return `${colorMethod(data.method)} ${colorStatus(data.statusCode)}`;
},
})
);Environment Examples
// Development
app.use(logger({ colorize: true, level: "verbose" }));
// Production
app.use(logger({ colorize: false, skip: ["/health"] }));
// Testing
app.use(logger({ enabled: false }));