Repository Public
1import { ApiError, fail, ok } from "../http";23export interface RouteCtx<C> { req: Request; url: URL; params: Record<string, string>; ctx: C }4type Handler<C> = (r: RouteCtx<C>) => Promise<unknown>;5export type RouteDef<C> = [method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", pattern: string, handler: Handler<C>];67/**8 * One catch-all route per API surface. The guard runs before ANY handler is looked up, so a handler cannot be9 * reached without it (no admin route can forget its authentication check).10 */11export function makeRouter<C>(routes: RouteDef<C>[], guard: (req: Request) => Promise<C>) {12 const compiled = routes.map(([method, pattern, handler]) => ({ method, parts: pattern.split("/").filter(Boolean), handler }));13 return async (req: Request, context: { params: Promise<{ path?: string[] }> }): Promise<Response> => {14 try {15 const ctx = await guard(req);16 const path = (await context.params).path ?? [];17 let pathMatched = false;18 for (const r of compiled) {19 if (r.parts.length !== path.length) continue;20 const params: Record<string, string> = {};21 if (!r.parts.every((p, i) => (p.startsWith(":") ? ((params[p.slice(1)] = path[i]), true) : p === path[i]))) continue;22 pathMatched = true;23 if (r.method !== req.method) continue;24 const out = await r.handler({ req, url: new URL(req.url), params, ctx });25 return out instanceof Response ? out : ok(out ?? { ok: true });26 }27 throw pathMatched ? new ApiError(405, "method_not_allowed") : new ApiError(404, "not_found");28 } catch (e) { return fail(e); }29 };30}31