Place a use.ts file in any folder, and its middleware automatically wraps all routes in that folder and its subfolders - no imports or wiring required.
How it Works
api/users/
├── about/
│ └── index.ts
├── account/
│ ├── index.ts
│ └── use.ts
├── index.ts
└── use.tsusers/use.tswraps all routes under/api/usersusers/account/use.tswraps only routes under/api/users/account
Execution order for a request to /api/users/account:
api/use.ts -> global middleware
users/use.ts -> parent folder
users/account/use.ts -> current folder
users/account/index.ts -> route handlerParent middleware always runs before child middleware.
Child routes can't skip parent
use.ts
The seeded boilerplate when you create a use.ts in an api/ subfolder:
import { use } from "_/api";
export type UseT = {};
export default [
use<UseT>(async (ctx, next) => {
return next();
})
];Some editors load the seeded content immediately, others require a brief unfocus/refocus.
Beside the default exported middleware, every use.ts in an api/ subfolder exports the UseT type - even if empty. This type extends the context for all routes underneath, giving you automatic type safety for anything the middleware adds.
The global api/use.ts is the exception: it may export UseT, but the export is ignored there - global middleware types come from api/env.d.ts instead.
Type-Safe Context Extension
The whole point of cascading middleware is to avoid manual wiring. That applies to types too - if your auth middleware adds user to the context, every route underneath should know about it without importing or declaring anything.
UseT makes this work. Define what your middleware adds:
// Hono: api/users/use.ts
import { HTTPException } from "hono/http-exception";
import { use } from "_/api";
export type UseT = {
user: { id: number; role: "admin" | "user" };
};
export default [
use<UseT>(async (ctx, next) => {
const token = ctx.req.header("authorization")?.replace("Bearer ", "");
// validate before adding to context - UseT promises this property exists
if (!token) throw new HTTPException(401, { message: "Authentication required" });
ctx.set("user", await verifyToken(token));
return next();
})
];Now every route under /api/admin has user typed on the context automatically - no imports, no type arguments on defineRoute:
// Hono: api/users/use.ts
export default defineRoute<"admin/dashboard">(({ GET }) => [
GET(async (ctx) => {
const user = ctx.get("user"); // typed as { id: number; role: "admin" | "user" }
}),
]);UseT is imported from each use.ts in the hierarchy and merged into the context type for defineRoute. Inner definitions override outer ones - just like at runtime, where inner middleware runs after outer middleware and can overwrite context values.
The global
api/use.tsdoes not need to exportUseT. Even if it does, the export is ignored - global middleware operates on types defined inapi/env.d.ts.UseTis foruse.tsfiles inapi/subfolders only, where the types cascade alongside the middleware itself.
Tip: inner use.ts files can import UseT from outer ones, extend it, and re-export - avoiding duplicate type definitions across the hierarchy:
import type { UseT as ParentT } from "../use";
export type UseT = ParentT & {
settingsAccess: "read" | "write";
};Parameter Availability
Cascading middleware runs for all routes in the hierarchy, including ones that don't define the parameters you might expect:
api/users/
├── [id]/index.ts ← has 'id' param
├── index.ts ← NO 'id' param
└── use.tsctx.params.id is undefined for /users. Keep cascading middleware generic - authentication, logging, rate limiting. Parameter-specific logic belongs in the route handler.
Multiple Middleware + Method Filtering
A single use.ts can define multiple functions, and each supports the on option to run only on specific request method(s):
// api/users/use.ts
import { use } from "_/api";
export type UseT = {
user: { id: number; name: string };
};
export default [
use<UseT>(async (ctx, next) => {
// will run on ANY request method
return next();
}),
use<UseT>(
async (ctx, next) => {
// will run only on POST
},
{ on: ["POST"] },
),
];Common Use Cases
Cascading middleware is where subtree-wide concerns belong: authentication, permission checks, audit logging, per-section rate limiting.
KosmoJS imposes nothing here - use accepts your framework's own middleware signature, so any Hono/H3/Koa middleware package works unchanged. There is nothing KosmoJS-specific about the middleware itself.
Third-party middleware
Add middleware to use.ts and it will run on every route underneath:
// Hono: api/users/use.ts
import { rateLimiter } from "hono-rate-limiter";
import { use } from "_/api";
export default [
use(
rateLimiter({
windowMs: 15 * 60 * 1000,
limit: 100,
keyGenerator: (ctx) => ctx.req.header("x-forwarded-for") ?? "anonymous",
}),
),
];api/app.ts takes a callback receiving the native app instance - not the array of use() calls above - so third-party middleware is registered exactly as that framework documents it.
Not CORS, though
A use.ts file is composed into each route's chain, so it only runs once a route has matched. A preflight OPTIONS is answered before that, and never reaches it. CORS belongs in api/app.ts instead.
Authentication for a subtree
Gate a whole section of the API by dropping a use.ts into its folder. Everything under /api/admin is now behind the check, and every route beneath it gets user typed on the context through UseT - no imports, no type arguments:
api/
├── use.ts -> global: request id, auth, rate limit
└── admin/
├── use.ts -> auth: everything under /api/admin
├── index.ts
└── users/
└── index.tsimport { use } from "_/api";
export type UseT = {
user: { id: number; role: "admin" | "user" };
};
export default [
use<UseT>(async function requireAdmin(ctx, next) {
// verify, then populate - UseT promises the property exists downstream
return next();
}),
];There is no bundled auth solution and no NextAuth-style integration: you verify the token and populate the context yourself, the native way for your framework (ctx.set("user", ...) on Hono, event.context.user = ... on H3, ctx.state.user = ... on Koa).
Choosing between use.ts and route-level use
Use a cascading use.ts | Use an inline use | |
|---|---|---|
| Scope | a folder and everything beneath it | one route file |
| Wiring | automatic - no imports | explicit, inside defineRoute |
| Context types | cascade via UseT | via defineRoute type arguments |
| Good for | auth, audit logging, rate limiting | one-off concerns for a single endpoint |
Keep cascading middleware generic. It runs for sibling routes too, so a param like id may be undefined there - see Parameter Availability. Parameter-specific logic belongs in the route handler.