diff --git a/backend/src/lib/router.ts b/backend/src/lib/router.ts new file mode 100644 index 0000000..671c9c1 --- /dev/null +++ b/backend/src/lib/router.ts @@ -0,0 +1,81 @@ +import { NextResponse } from 'next/server'; + +export type HandlerFn = (ctx: HandlerContext) => unknown | Promise; + +export type HandlerContext = { + params: Record; + method: string; + body: unknown; + searchParams: URLSearchParams; +}; + +type RouteEntry = { + pattern: string[]; + methods: string[]; + handler: HandlerFn; +}; + +const routes: RouteEntry[] = []; + +export function route(pattern: string, methods: string[], handler: HandlerFn) { + const parts = pattern.split('/').filter(Boolean); + routes.push({ pattern: parts, methods: methods.map(m => m.toUpperCase()), handler }); +} + +export async function dispatch( + request: Request, + pathSegments: string[], + method: string +): Promise { + for (const entry of routes) { + if (!entry.methods.includes(method)) continue; + const params: Record = {}; + let match = true; + + if (entry.pattern.length !== pathSegments.length) continue; + + for (let i = 0; i < entry.pattern.length; i++) { + const ep = entry.pattern[i]; + const sp = pathSegments[i]; + if (ep.startsWith(':')) { + params[ep.slice(1)] = sp; + } else if (ep !== sp) { + match = false; + break; + } + } + + if (!match) continue; + + let body: unknown = undefined; + const ct = request.headers.get('content-type') || ''; + if (ct.includes('application/json')) { + try { body = await request.json(); } catch { body = undefined; } + } + + const ctx: HandlerContext = { + params, + method, + body, + searchParams: new URL(request.url).searchParams, + }; + + try { + const result = await entry.handler(ctx); + return NextResponse.json(result, { status: 200 }); + } catch (err: any) { + const status = err.status || 500; + return NextResponse.json({ error: err.message || 'Internal error' }, { status }); + } + } + + return NextResponse.json({ error: 'Not found' }, { status: 404 }); +} + +export class HttpError extends Error { + status: number; + constructor(status: number, message: string) { + super(message); + this.status = status; + } +}