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