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 cannot skip parent use.ts.
The generated boilerplate when you create a new use.ts:
import { use } from "_/api";
export type UseT = {};
export default [
use<UseT>(async (ctx, next) => {
return next();
})
];Some editors load the generated content immediately, others require a brief unfocus/refocus.
Beside the default exported middleware, every use.ts 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.
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:
import { use } from "_/api";
export type UseT = {
user: { id: number; role: "admin" | "user" };
};
export default [
use<UseT>(async (ctx, next) => {
const token = ctx.headers.authorization?.replace("Bearer ", "");
// NOTE: validate before adding to state - UseT promises this property exists
ctx.assert(token, 401, "Authentication required");
ctx.state.user = await verifyToken(token);
return next();
})
];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 ", "");
// NOTE: 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:
export default defineRoute<"admin/dashboard">(({ GET }) => [
GET(async (ctx) => {
const { user } = ctx.state; // typed as { id: number; role: "admin" | "user" }
}),
]);export default defineRoute<"admin/dashboard">(({ GET }) => [
GET(async (ctx) => {
const user = ctx.get("user"); // typed as { id: number; role: "admin" | "user" }
}),
]);The code generator imports UseT from each use.ts in the hierarchy and merges them 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.
Note: the global api/use.ts does not need to export UseT. Even if it does, the export is ignored - global middleware operates on DefaultState (Koa) or DefaultVariables (Hono) defined in api/env.d.ts. UseT is for folder-level use.ts files only, where the types cascade alongside the middleware itself.
Tip: inner
use.tsfiles can importUseTfrom outer ones, extend it, and re-export - avoiding duplicate type definitions across the hierarchy:tsimport type { UseT as ParentT } from "../use"; export type UseT = ParentT & { settingsAccess: "read" | "write"; };
Common Use Cases β
Authentication β
import { use } from "_/api";
export type UseT = {
user: { id: number; name: string; role: string };
};
export default [
use<UseT>(async (ctx, next) => {
const token = ctx.headers.authorization?.replace("Bearer ", "");
ctx.assert(token, 401, "Authentication required");
const user = await verifyToken(token);
ctx.assert(user.role === "admin", 403, "Admin access required");
ctx.state.user = user;
return next();
})
];import { HTTPException } from "hono/http-exception";
import { use } from "_/api";
export type UseT = {
user: { id: number; name: string; role: string };
};
export default [
use<UseT>(async (ctx, next) => {
const token = ctx.req.header("authorization")?.replace("Bearer ", "");
if (!token) throw new HTTPException(401, { message: "Authentication required" });
const user = await verifyToken(token);
if (user.role !== "admin") throw new HTTPException(403, { message: "Admin access required" });
ctx.set("user", user);
return next();
})
];Every route under /api/admin now requires admin auth. Route handlers can assume the user is already validated (ctx.state.user for Koa, ctx.get("user") for Hono).
Request Logging β
import { use } from "_/api";
export type UseT = {
requestId: string;
};
export default [
use<UseT>(async (ctx, next) => {
const start = Date.now();
const requestId = crypto.randomUUID();
ctx.state.requestId = requestId;
console.log(`[${requestId}] ${ctx.method} ${ctx.path}`);
try {
await next();
} finally {
console.log(`[${requestId}] completed in ${Date.now() - start}ms`);
}
})
];import { use } from "_/api";
export type UseT = {
requestId: string;
};
export default [
use<UseT>(async (ctx, next) => {
const start = Date.now();
const requestId = crypto.randomUUID();
ctx.set("requestId", requestId);
console.log(`[${requestId}] ${ctx.req.method} ${ctx.req.path}`);
await next();
console.log(`[${requestId}] completed in ${Date.now() - start}ms`);
})
];Rate Limiting β
import rateLimit from "koa-ratelimit";
import { use } from "_/api";
export type UseT = {};
export default [
use<UseT>(
rateLimit({
driver: "memory",
db: new Map(),
duration: 60000,
max: 100,
})
)
];import { rateLimiter } from "hono-rate-limiter";
import { use } from "_/api";
export type UseT = {};
export default [
use<UseT>(
rateLimiter({
windowMs: 60000,
limit: 100,
keyGenerator: (ctx) => ctx.req.header("x-forwarded-for") ?? "",
}),
)
];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:
import { use } from "_/api";
export type UseT = {
user: { id: number; name: string };
};
export default [
use<UseT>(async (ctx, next) => {
console.log(`${ctx.method} ${ctx.path}`);
return next();
}),
use<UseT>(
async (ctx, next) => {
const token = ctx.headers.authorization?.replace("Bearer ", "");
ctx.assert(token, 401, "Authentication required");
ctx.state.user = await verifyToken(token);
return next();
},
{ on: ["POST", "PUT", "PATCH", "DELETE"] },
),
use<UseT>(async (ctx, next) => {
const start = Date.now();
await next();
ctx.set("X-Response-Time", `${Date.now() - start}ms`);
}),
];import { HTTPException } from "hono/http-exception";
import { use } from "_/api";
export type UseT = {
user: { id: number; name: string };
};
export default [
use<UseT>(async (ctx, next) => {
console.log(`${ctx.req.method} ${ctx.req.path}`);
return next();
}),
use<UseT>(
async (ctx, next) => {
const token = ctx.req.header("authorization")?.replace("Bearer ", "");
if (!token) throw new HTTPException(401, { message: "Authentication required" });
ctx.set("user", await verifyToken(token));
return next();
},
{ on: ["POST", "PUT", "PATCH", "DELETE"] },
),
use<UseT>(async (ctx, next) => {
const start = Date.now();
await next();
ctx.header("X-Response-Time", `${Date.now() - start}ms`);
}),
];