diff --git a/src/index.ts b/src/index.ts index 72e78fc3f..a5e224044 100644 --- a/src/index.ts +++ b/src/index.ts @@ -73,6 +73,10 @@ export { export { defineRoute } from "./utils/route.ts"; export type { RouteDefinition } from "./utils/route.ts"; +// Middleware Route +export { defineMiddlewareRoute } from "./utils/middleware-route.ts"; +export type { MiddlewareRouteDefinition } from "./utils/middleware-route.ts"; + // Request export { getRequestHost, diff --git a/src/middleware.ts b/src/middleware.ts index 98c3d5f3f..fb38ec07d 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -14,16 +14,30 @@ export function normalizeMiddleware( opts: MiddlewareOptions & { route?: string } = {}, ): Middleware { const matcher = createMatcher(opts); + const hasMeta = opts.meta && Object.keys(opts.meta).length > 0; + if ( !matcher && + !hasMeta && (input.length > 1 || input.constructor?.name === "AsyncFunction") ) { return input; // Fast path: async or with explicit next() and no matcher filters } + return (event, next) => { if (matcher && !matcher(event)) { return next(); } + + // Add meta to event context if provided + if (hasMeta) { + event.context.matchedMiddleware = event.context.matchedMiddleware || []; + event.context.matchedMiddleware.push({ + route: opts.route, + meta: opts.meta, + }); + } + const res = input(event, next); return res === undefined || res === kNotFound ? next() : res; }; diff --git a/src/types/context.ts b/src/types/context.ts index b8f5a9964..b35aa05cc 100644 --- a/src/types/context.ts +++ b/src/types/context.ts @@ -1,5 +1,5 @@ import type { Session } from "../utils/session.ts"; -import type { H3Route } from "./h3.ts"; +import type { H3Route, H3RouteMeta } from "./h3.ts"; export interface H3EventContext extends Record { /* Matched router parameters */ @@ -15,6 +15,16 @@ export interface H3EventContext extends Record { */ matchedRoute?: H3Route; + /** + * Matched middleware with their metadata + * + * @experimental The object structure may change in non-major version. + */ + matchedMiddleware?: Array<{ + route?: string; + meta?: H3RouteMeta; + }>; + /* Cached session data */ sessions?: Record; diff --git a/src/types/h3.ts b/src/types/h3.ts index d87612f63..b140987bf 100644 --- a/src/types/h3.ts +++ b/src/types/h3.ts @@ -58,6 +58,7 @@ export type RouteOptions = { export type MiddlewareOptions = { method?: string; match?: (event: H3Event) => boolean; + meta?: H3RouteMeta; }; export declare class H3 { diff --git a/src/utils/middleware-route.ts b/src/utils/middleware-route.ts new file mode 100644 index 000000000..ca494a57a --- /dev/null +++ b/src/utils/middleware-route.ts @@ -0,0 +1,81 @@ +import type { HTTPMethod } from "../types/h3.ts"; +import type { Middleware } from "../types/handler.ts"; +import type { H3Plugin, H3 } from "../types/h3.ts"; +import type { H3Event } from "../event.ts"; +import { defineMiddleware } from "../middleware.ts"; + +/** + * Middleware route definition options + */ +export interface MiddlewareRouteDefinition { + /** + * Path pattern for the middleware, e.g. '/api/**' + */ + path?: string; + + /** + * HTTP methods to apply the middleware to + */ + methods?: HTTPMethod[]; + + /** + * Middleware handler function + */ + handler: Middleware; + + /** + * Additional middleware metadata + */ + meta?: Record; +} + +/** + * Define a middleware route as a plugin that can be registered with app.register() + * + * @example + * ```js + * const authMiddleware = defineMiddlewareRoute({ + * path: '/api/**', + * methods: ['GET', 'POST'], + * meta: { + * rateLimit: { + * interval: '1m', + * tokensPerInterval: 10, + * }, + * }, + * handler: async (event, next) => { + * console.log('Auth middleware running'); + * // Check authentication + * if (!event.context.user) { + * return new Response('Unauthorized', { status: 401 }); + * } + * return next(); + * } + * }); + * + * app.register(authMiddleware); + * ``` + */ +export function defineMiddlewareRoute( + def: MiddlewareRouteDefinition, +): H3Plugin { + const middleware = defineMiddleware(def.handler); + + return (h3: H3) => { + const options = { + ...(def.methods && { + match: (event: H3Event) => { + const method = event.req.method.toUpperCase(); + return def.methods!.includes(method as HTTPMethod); + }, + }), + ...(def.meta && { meta: def.meta }), + }; + + if (def.path) { + h3.use(def.path, middleware, options); + } else { + h3.use(middleware, options); + } + }; +}