interface RouterContext { url: URL; params: Record; state: Record; pattern: URLPattern; request: Request; } type Handler = (req: Request, ctx: RouterContext) => Promise | Response; type Middleware = (ctx: RouterContext, next: () => Promise) => Promise; interface RouteConfig { pattern: URLPattern; handlers: { [method: string]: Handler }; } interface MiddlewareConfig { pattern: URLPattern; handler: Middleware; // Add path for sorting path: string; } interface RouteConfigurator { get(handler: Handler): RouteConfigurator; post(handler: Handler): RouteConfigurator; put(handler: Handler): RouteConfigurator; delete(handler: Handler): RouteConfigurator; patch(handler: Handler): RouteConfigurator; options(handler: Handler): RouteConfigurator; } /** * A simple router for Deno * * @author Emmaline Autumn * * @example * ```ts * const router = new Router(); * router.route('/users') * .get((ctx) => { * return new Response('GET /users'); * }) * .post((ctx) => { * return new Response('POST /users'); * }); * * router.route('/users/:id') * .get((ctx) => { * return new Response(`GET /users/${ctx.params.id}`); * }) * .put((ctx) => { * return new Response(`PUT /users/${ctx.params.id}`); * }) * .delete((ctx) => { * return new Response(`DELETE /users/${ctx.params.id}`); * }); * * router.route('/posts') * .get((ctx) => { * return new Response('GET /posts'); * }) * .post((ctx) => { * return new Response('POST /posts'); * }); * * router.route('/posts/:id') * .get((ctx) => { * return new Response(`GET /posts/${ctx.params.id}`); * }) * .put((ctx) => { * return new Response(`PUT /posts/${ctx.params.id}`); * }) * .delete((ctx) => { * return new Response(`DELETE /posts/${ctx.params.id}`); * }); * * router.route('/*') * .get((ctx) => { * return new Response('GET /*'); * }) * .post((ctx) => { * return new Response('POST /*'); * }); * * router.use('/users', async (_, next) => { * console.log('Using middleware'); * return await next(); * }); * * Deno.serve({ * port: 8000, * handler: router.handle * }); */ export class Router { private routes: RouteConfig[] = []; private middleware: MiddlewareConfig[] = []; route(path: string): RouteConfigurator { path = path.startsWith('/') ? path : `/${path}`; const pattern = new URLPattern({ pathname: path }); const routeConfig: RouteConfig = { pattern, handlers: {} }; this.routes.push(routeConfig); return { get(handler: Handler) { routeConfig.handlers['GET'] = handler; return this; }, post(handler: Handler) { routeConfig.handlers['POST'] = handler; return this; }, put(handler: Handler) { routeConfig.handlers['PUT'] = handler; return this; }, delete(handler: Handler) { routeConfig.handlers['DELETE'] = handler; return this; }, patch(handler: Handler) { routeConfig.handlers['PATCH'] = handler; return this; }, options(handler: Handler) { routeConfig.handlers['OPTIONS'] = handler; return this; } }; } use(pathOrMiddleware: string | Middleware, middlewareOrRouter?: Middleware | Router): Router { // Handle the case where only middleware is provided if (typeof pathOrMiddleware === 'function') { const pattern = new URLPattern({ pathname: '/*' }); this.middleware.push({ pattern, handler: pathOrMiddleware, path: '/*' }); return this; } const path = pathOrMiddleware; const middleware = middlewareOrRouter; if (!middleware) { throw new Error('Middleware or Router is required'); } // Normalize the path to handle both exact matches and nested paths const normalizedPath = path.startsWith('/') ? path : `/${path}`; const isParameterPath = normalizedPath.includes(':'); const wildcardPath = isParameterPath ? normalizedPath : normalizedPath === '/' ? '/*' : `${normalizedPath.replace(/\/+$/, '')}/*?`; if (middleware instanceof Router) { // Merge the nested router's routes for (const nestedRoute of middleware.routes) { const combinedPath = this.combinePaths(path, nestedRoute.pattern.pathname); const newPattern = new URLPattern({ pathname: combinedPath }); this.routes.push({ pattern: newPattern, handlers: { ...nestedRoute.handlers } }); } // Merge the nested router's middleware for (const nestedMiddleware of middleware.middleware) { const combinedPath = this.combinePaths(path, nestedMiddleware.path); const newPattern = new URLPattern({ pathname: combinedPath }); this.middleware.push({ pattern: newPattern, handler: nestedMiddleware.handler, path: combinedPath }); } } else { // Handle regular middleware const pattern = new URLPattern({ pathname: wildcardPath }); this.middleware.push({ pattern, handler: middleware, path: wildcardPath }); } return this; } handle = async (req: Request): Promise => { const url = new URL(req.url); const method = req.method; // Find the matching route const matchedRoute = this.findMatchingRoute(url); if (!matchedRoute) { return new Response('Not Found', { status: 404 }); } const handler = matchedRoute.config.handlers[method]; if (!handler) { return new Response('Method Not Allowed', { status: 405 }); } // Get matching middleware and sort by path specificity const matchingMiddleware = this.getMatchingMiddleware(url); // Create the base context object const baseCtx: RouterContext = { url, params: {}, state: {}, pattern: matchedRoute.config.pattern, request: req }; // Combine route parameters with the base context baseCtx.params = { ...baseCtx.params, ...matchedRoute.params }; // Execute middleware chain let index = 0; const executeMiddleware = async (): Promise => { if (index < matchingMiddleware.length) { const { middleware, params } = matchingMiddleware[index++]; // Create a new context for each middleware with its specific parameters const middlewareCtx: RouterContext = { ...baseCtx, params: { ...baseCtx.params, ...params }, pattern: middleware.pattern }; return await middleware.handler(middlewareCtx, executeMiddleware); } // Final handler gets the accumulated parameters return await handler(req, baseCtx); }; try { return await executeMiddleware(); } catch (error) { console.error('Error handling request:', error); return new Response('Internal Server Error', { status: 500 }); } } private findMatchingRoute(url: URL): { config: RouteConfig; params: Record } | null { for (const route of this.routes) { const result = route.pattern.exec(url); if (result) { return { config: route, params: result.pathname.groups }; } } return null; } private getMatchingMiddleware(url: URL): Array<{ middleware: MiddlewareConfig; params: Record }> { const matches = this.middleware .map(mw => { const result = mw.pattern.exec(url) ?? mw.pattern.exec(url.href + '/'); // console.log(url, mw.pattern, result); if (result) { return { middleware: mw, params: result.pathname.groups }; } return null; }) .filter((item): item is { middleware: MiddlewareConfig; params: Record } => item !== null); // Sort middleware by path specificity return matches.sort((a, b) => { const pathA = a.middleware.path; const pathB = b.middleware.path; // Global middleware comes first if (pathA === '/*' && pathB !== '/*') return -1; if (pathB === '/*' && pathA !== '/*') return 1; // More specific paths (with more segments) come later const segmentsA = pathA.split('/').length; const segmentsB = pathB.split('/').length; if (segmentsA !== segmentsB) { return segmentsA - segmentsB; } // If same number of segments, longer paths are more specific return pathA.length - pathB.length; }); } private combinePaths(basePath: string, routePath: string): string { const normalizedBase = basePath.replace(/\/+$/, '').replace(/^\/*/, '/'); const normalizedRoute = routePath.replace(/^\/*/, ''); return normalizedBase === '/' ? `/${normalizedRoute}` : `${normalizedBase}/${normalizedRoute}`; } } export default Router; if (import.meta.main) { console.log('Starting server...'); const router = new Router(); router.route('/users') .get((ctx) => { return new Response('GET /users'); }) // .post((ctx) => { // return new Response('POST /users'); // }); // router.route('/users/:id') // .get((ctx) => { // return new Response(`GET /users/${ctx.params.id}`); // }) // .put((ctx) => { // return new Response(`PUT /users/${ctx.params.id}`); // }) // .delete((ctx) => { // return new Response(`DELETE /users/${ctx.params.id}`); // }); // router.route('/posts') // .get((ctx) => { // return new Response('GET /posts'); // }) // .post((ctx) => { // return new Response('POST /posts'); // }); // router.route('/posts/:id') // .get((ctx) => { // return new Response(`GET /posts/${ctx.params.id}`); // }) // .put((ctx) => { // return new Response(`PUT /posts/${ctx.params.id}`); // }) // .delete((ctx) => { // return new Response(`DELETE /posts/${ctx.params.id}`); // }); // router.route('/*') // .get((ctx) => { // return new Response('GET /*'); // }) // .post((ctx) => { // return new Response('POST /*'); // }); router.use('/users', async (_, next) => { console.log('Using middleware'); return await next(); }); Deno.serve({ port: 8000, handler: router.handle }); }