Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions packages/framework/src/define-handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { expect, expectTypeOf, test } from "vitest";

import { defineHandler } from "./define-handler";
import type { HandlerModule } from "./index";

declare module "./index" {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface RouteMap {
// eslint-disable-next-line @typescript-eslint/naming-convention
"/api/health": Record<string, never>;
// eslint-disable-next-line @typescript-eslint/naming-convention
"/api/users/[id]": { id: string };
}
}

// --- TDD Slice 1: identity at runtime ---

test("defineHandler returns config unchanged (identity)", () => {
const config = {
// eslint-disable-next-line @typescript-eslint/naming-convention
GET: () => Response.json({ ok: true }),
};

const result = defineHandler("/api/health")(config);

expect(result).toBe(config);
});

// --- TDD Slice 2: supports all HTTP methods ---

test("defineHandler accepts all HTTP method handlers", () => {
const config = {
// eslint-disable-next-line @typescript-eslint/naming-convention
GET: () => new Response("get"),
// eslint-disable-next-line @typescript-eslint/naming-convention
POST: () => new Response("post"),
// eslint-disable-next-line @typescript-eslint/naming-convention
PUT: () => new Response("put"),
// eslint-disable-next-line @typescript-eslint/naming-convention
PATCH: () => new Response("patch"),
// eslint-disable-next-line @typescript-eslint/naming-convention
DELETE: () => new Response("delete"),
};

const result = defineHandler("/api/health")(config);

expect(result).toBe(config);
});

// --- TDD Slice 3: all methods are optional ---

test("defineHandler allows omitting all methods", () => {
const result = defineHandler("/api/health")({});

expect(result).toEqual({});
});

// --- TDD Slice 4: type — params are typed from RouteMap ---

test("handler receives typed params from RouteMap", () => {
defineHandler("/api/users/[id]")({
// eslint-disable-next-line @typescript-eslint/naming-convention
GET: (ctx) => {
expectTypeOf(ctx.params).toEqualTypeOf<{ id: string }>();
expectTypeOf(ctx.request).toEqualTypeOf<Request>();
return Response.json({ id: ctx.params.id });
},
});
});

// --- TDD Slice 5: type — return type is HandlerModule ---

test("defineHandler returns a HandlerModule", () => {
const result = defineHandler("/api/users/[id]")({
// eslint-disable-next-line @typescript-eslint/naming-convention
GET: () => Response.json({ ok: true }),
});

expectTypeOf(result).toExtend<HandlerModule<{ id: string }>>();
});

// --- TDD Slice 6: type — handlers accept async functions ---

test("handlers can return Promise<Response>", () => {
const result = defineHandler("/api/health")({
// eslint-disable-next-line @typescript-eslint/naming-convention
POST: async (ctx) => {
const body = (await ctx.request.json()) as Record<string, unknown>;
return Response.json({ received: body });
},
});

expectTypeOf(result.POST).toExtend<
| ((ctx: { request: Request; params: Record<string, never> }) => Response | Promise<Response>)
| undefined
>();
});

// --- TDD Slice 7: type — paramless route has empty params ---

test("paramless route has Record<string, never> params", () => {
defineHandler("/api/health")({
// eslint-disable-next-line @typescript-eslint/naming-convention
GET: (ctx) => {
expectTypeOf(ctx.params).toEqualTypeOf<Record<string, never>>();
return Response.json({ ok: true });
},
});
});
5 changes: 5 additions & 0 deletions packages/framework/src/define-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { HandlerModule, RouteMap } from "./core/index";

export function defineHandler<TPath extends keyof RouteMap>(_path: TPath) {
return (config: HandlerModule<RouteMap[TPath]>) => config;
}
1 change: 1 addition & 0 deletions packages/framework/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export const VERSION = "0.0.0";

export { createApp } from "./create-app";
export type { AppConfig } from "./create-app";
export { defineHandler } from "./define-handler";
export { definePage } from "./define-page";

export type {
Expand Down
Loading