Beyond the standard HTTP method handlers, you often need to run custom middleware - code that executes before your main handler to perform tasks like authentication, logging, or data transformation.
Basic Usage
KosmoJS provides the use function for applying middleware. The same API applies identically to all frameworks, only the middleware internals slightly differs by framework:
export default defineRoute<"example">(({ GET, POST, use }) => [
use(async (ctx, next) => {
// runs for both GET and POST
return next();
}),
GET(async (ctx) => { /* ... */ }),
POST(async (ctx) => { /* ... */ }),
]);Middleware must call next() to pass control to the next layer. Skipping next() short-circuits the chain - useful for early rejections.
Execution Order (Onion Model)
Middleware runs in definition order going in, then unwinds in reverse after the handler.
Consider this example:
export default defineRoute<"example">(({ POST, use }) => [
use(async (ctx, next) => {
console.log("First middleware");
await next();
console.log("First middleware after next");
}),
use(async (ctx, next) => {
console.log("Second middleware");
await next();
console.log("Second middleware after next");
}),
POST(async (ctx) => {
console.log("POST handler");
// ...
}),
]);When a POST request arrives, the execution order is like:
First middleware
Second middleware
POST handler
Second middleware after next
First middleware after nextGlobal middleware from api/use.ts runs first, then route-level use calls, then the handler.
Positioning note: All
usecalls run before method handlers regardless of where they appear in the array. Defininguseafter a handler doesn't change this:
export default defineRoute(({ use, GET, POST }) => [
use(firstMiddleware),
GET(async (ctx) => { /* ... */ }),
POST(async (ctx) => { /* ... */ }),
use(secondMiddleware), // still runs BEFORE handlers
]);Method-Specific Middleware
Use the on option to restrict middleware to specific HTTP methods:
export default defineRoute<"example">(({ GET, POST, use }) => [
use(async (ctx, next) => {
ctx.state.user = await verifyToken(ctx.headers.authorization);
return next();
}, {
on: ["POST"],
}),
GET(async (ctx) => {
// no auth required
}),
POST(async (ctx) => {
// ctx.state.user is available
}),
]);Slot Composition
Slots are named positions in the middleware chain. Middleware with the same slot name replaces earlier middleware at that position - useful for overriding global defaults per-route.
A global error handler defined in api/use.ts:
export default [
use(
async (ctx, next) => { /* global logger */ },
{ slot: "logger" },
),
];Override it for a specific route:
export default defineRoute<"upload">(({ POST, use }) => [
use(
async (ctx, next) => {
// custom logger for this route only
},
{ slot: "logger" },
),
POST(async (ctx) => { /* ... */ }),
]);Important: When overriding via slot, explicitly set
onif needed - it doesn't inherit from the middleware being replaced.
Custom slot names, like logger, should be added to api/env.d.ts:
export declare module "@kosmojs/core/api" {
interface UseSlots {
logger: string;
}
}Then use it anywhere:
use(async (ctx, next) => { /* ... */ }, { slot: "logger" })