feat: add API router
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user