feat: add API router

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
achmad
2026-05-29 16:31:48 +07:00
parent 0bd3cb3bb4
commit 2e010af1cf
+81
View File
@@ -0,0 +1,81 @@
import { NextResponse } from 'next/server';
export type HandlerFn = (ctx: HandlerContext) => unknown | Promise<unknown>;
export type HandlerContext = {
params: Record<string, string>;
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<NextResponse> {
for (const entry of routes) {
if (!entry.methods.includes(method)) continue;
const params: Record<string, string> = {};
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;
}
}