Skip to content

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:

typescript
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 → Response

Adding Middleware

Use app.use() to register global middleware:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
app.onRequest((c) => {
  c.store.startTime = Date.now();
  console.log(`→ ${c.method} ${c.path}`);
});

Return a Response to stop processing:

typescript
app.onRequest((c) => {
  if (c.path.startsWith("/admin") && !isAdmin(c)) {
    return new Response("Forbidden", { status: 403 });
  }
});

onResponse

Runs after the handler:

typescript
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)
typescript
// 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:

typescript
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

typescript
import { Kyrin, cors } from "kyrin";

const app = new Kyrin();

app.use(cors()); // Add CORS plugin

Plugin Structure

A plugin is an object with optional middleware, onRequest, and onResponse:

typescript
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:

typescript
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:

typescript
import { Kyrin, cors } from "kyrin";

const app = new Kyrin();
app.use(cors());

Configuration Options

typescript
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:

typescript
app.use(
  cors({
    origin: "https://example.com",
  })
);

Allow multiple origins:

typescript
app.use(
  cors({
    origin: ["https://example.com", "https://admin.example.com"],
  })
);

Dynamic origin validation:

typescript
app.use(
  cors({
    origin: (origin) => {
      return origin.endsWith(".example.com");
    },
  })
);

With Credentials

When your frontend needs to send cookies:

typescript
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-Origin
  • Access-Control-Allow-Methods
  • Access-Control-Allow-Headers
  • Access-Control-Max-Age
  • Access-Control-Allow-Credentials (if enabled)

Logger Plugin

NestJS-style logging middleware for Kyrin framework.

bash
bun add @kyrinjs/logger

Basic Usage

typescript
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 +2ms

Configuration

typescript
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

typescript
// 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: verbosedebuglogwarnerror

typescript
// Only show warnings and errors
app.use(logger({ level: "warn" }));

Status Code Colors

RangeColorMeaning
2xxGreenSuccess
3xxCyanRedirect
4xxYellowClient Error
5xxRedServer Error

Custom Format

typescript
app.use(
  logger({
    format: (data) => `${data.method} ${data.path} - ${data.statusCode}`,
  })
);

Available data: method, path, statusCode, duration, timestamp, context

Color Utilities

typescript
import { colorMethod, colorStatus, formatDuration } from "@kyrinjs/logger";

app.use(
  logger({
    format: (data) => {
      return `${colorMethod(data.method)} ${colorStatus(data.statusCode)}`;
    },
  })
);

Environment Examples

typescript
// Development
app.use(logger({ colorize: true, level: "verbose" }));

// Production
app.use(logger({ colorize: false, skip: ["/health"] }));

// Testing
app.use(logger({ enabled: false }));

Released under the MIT License.