diff --git a/example/main.ts b/example/main.ts index bfb3201..447fab0 100644 --- a/example/main.ts +++ b/example/main.ts @@ -1,4 +1,4 @@ -import { HttpRouter } from "../mod.ts"; +import { createRouter } from "../mod.ts"; type User = { name: string }; const users: Record = { @@ -6,9 +6,7 @@ const users: Record = { "2": { name: "Jane Doe" }, }; -const listener = Deno.listen({ port: 8080 }); - -const router = new HttpRouter(); +const router = createRouter(); router.get("/users/:id", (_req, match) => { if (!match.pathname.groups.id) { @@ -38,4 +36,4 @@ router.all("*", (_match, _req) => { return Response.json({ message: "Not Found" }, { status: 404 }); }); -await router.serve(listener); +Deno.serve({ port: 8080 }, router); diff --git a/mod.ts b/mod.ts index acff726..fdd0691 100644 --- a/mod.ts +++ b/mod.ts @@ -1,2 +1,2 @@ -export type { HttpMethod, HttpRouteHandler } from "./src/router.ts"; -export { HttpRouter } from "./src/router.ts"; +export type { HttpRouter, HttpMethod, HttpRouteHandler } from "./src/router.ts"; +export { createRouter } from "./src/router.ts"; diff --git a/readme.md b/readme.md index 3f033d4..161d0b6 100644 --- a/readme.md +++ b/readme.md @@ -14,9 +14,7 @@ const users: Record = { "2": { name: "Jane Doe" }, }; -const listener = Deno.listen({ port: 8080 }); - -const router = new HttpRouter(); +const router = createRouter(); router.get("/users/:id", (_req, match) => { const user = users[match.pathname.groups.id]; @@ -38,5 +36,5 @@ router.all("*", (_match, _req) => { return Response.json({ message: "Not Found" }, { status: 404 }); }); -await router.serve(listener); +Deno.serve({ port: 8080 }, router); ``` diff --git a/src/router.ts b/src/router.ts index 14536d4..b7b2601 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,7 +1,4 @@ -export type HttpRouteHandler = ( - request: Request, - match: URLPatternResult, -) => Response | Promise; +export type HttpRouteHandler = (request: Request, match: URLPatternResult) => Response | Promise; type HttpRoute = { pattern: URLPattern; @@ -9,46 +6,43 @@ type HttpRoute = { method: string; }; -export type HttpMethod = - | "get" - | "post" - | "delete" - | "patch" - | "put" - | "options"; - -export class HttpRouter { - #routes: HttpRoute[] = []; - - get = (pattern: URLPatternInput, handler: HttpRouteHandler) => - this.#use("get", pattern, handler); - post = (pattern: URLPatternInput, handler: HttpRouteHandler) => - this.#use("post", pattern, handler); - put = (pattern: URLPatternInput, handler: HttpRouteHandler) => - this.#use("put", pattern, handler); - delete = (pattern: URLPatternInput, handler: HttpRouteHandler) => - this.#use("delete", pattern, handler); - - all = ( +export type HttpMethod = "get" | "post" | "delete" | "patch" | "put" | "options" | "head"; +export type HttpRouteMethod = (pattern: URLPatternInput, handler: HttpRouteHandler) => HttpRouter; + +export interface HttpRouter { + (request: Request): Promise; + all: ( pattern: URLPatternInput, - handler: Partial> | HttpRouteHandler, - ) => { + handler: Partial> | HttpRouteHandler + ) => HttpRouter; + get: HttpRouteMethod; + post: HttpRouteMethod; + put: HttpRouteMethod; + delete: HttpRouteMethod; + patch: HttpRouteMethod; + options: HttpRouteMethod; + head: HttpRouteMethod; + serve(listener: Deno.Listener): Promise; +} + +export function createRouter(): HttpRouter { + const routes: HttpRoute[] = []; + + const all = (pattern: URLPatternInput, handler: Partial> | HttpRouteHandler) => { if (typeof handler === "object") { for (const [method, h] of Object.entries(handler)) { - this.#use(method as HttpMethod, pattern, h); + use(method as HttpMethod)(pattern, h); } } else { - this.#use("all", pattern, handler); + use("all")(pattern, handler); } + + return instance; }; - #use = ( - method: HttpMethod | "all", - pattern: string | URLPatternInput, - handler: HttpRouteHandler, - ) => { + const use = (method: HttpMethod | "all") => (pattern: string | URLPatternInput, handler: HttpRouteHandler) => { if (typeof pattern === "string") { - this.#routes.push({ + routes.push({ method, pattern: new URLPattern({ protocol: "http{s}?", @@ -63,16 +57,15 @@ export class HttpRouter { handler, }); } else { - this.#routes.push({ method, pattern: new URLPattern(pattern), handler }); + routes.push({ method, pattern: new URLPattern(pattern), handler }); } + + return instance; }; - async handleRequest(request: Request): Promise { - for (const route of this.#routes) { - if ( - route.method.toUpperCase() === "ALL" || - route.method.toUpperCase() === request.method.toUpperCase() - ) { + async function handleRequest(request: Request): Promise { + for (const route of routes) { + if (route.method.toUpperCase() === "ALL" || route.method.toUpperCase() === request.method.toUpperCase()) { const url = new URL(request.url, "http://example.com"); const result = route.pattern.exec(url); @@ -86,20 +79,34 @@ export class HttpRouter { throw new Error("Unhandled request"); } - async handleEvent(event: Deno.RequestEvent) { - const response = await this.handleRequest(event.request); + async function handleEvent(event: Deno.RequestEvent) { + const response = await handleRequest(event.request); await event.respondWith(response); } - async handleConnection(conn: Deno.Conn) { + async function handleConnection(conn: Deno.Conn) { for await (const event of Deno.serveHttp(conn)) { - this.handleEvent(event); + handleEvent(event); } } - async serve(listener: Deno.Listener) { + async function serve(listener: Deno.Listener) { for await (const conn of listener) { - this.handleConnection(conn); + handleConnection(conn); } } + + const instance = Object.assign(handleRequest, { + get: use("get"), + post: use("post"), + delete: use("delete"), + patch: use("patch"), + put: use("put"), + options: use("options"), + head: use("head"), + all, + serve, + }); + + return instance; } diff --git a/src/router_test.ts b/src/router_test.ts index add4fe7..b04919d 100644 --- a/src/router_test.ts +++ b/src/router_test.ts @@ -1,9 +1,6 @@ -import { - assertEquals, - assertRejects, -} from "https://deno.land/std@0.198.0/assert/mod.ts"; +import { assertEquals, assertRejects } from "https://deno.land/std@0.198.0/assert/mod.ts"; -import { HttpRouter } from "./router.ts"; +import { createRouter } from "./router.ts"; const baseurl = "http://example.com"; @@ -12,46 +9,44 @@ function createRequest(path: string) { } Deno.test("get random json body", async () => { - const router = new HttpRouter(); + const router = createRouter(); const body = { message: crypto.randomUUID() }; router.get("/", () => Response.json(body)); - const response = await router.handleRequest( - new Request(new URL("/", baseurl)), - ); + const response = await router(new Request(new URL("/", baseurl))); assertEquals(await response.json(), body); }); Deno.test("match path parameter", async () => { - const router = new HttpRouter(); + const router = createRouter(); router.get("/", () => new Response("not found")); router.get("/:id", (_, match) => new Response(match.pathname.groups.id)); const id = crypto.randomUUID(); - const response1 = await router.handleRequest(createRequest("/")); - const response2 = await router.handleRequest(createRequest("/" + id)); + const response1 = await router(createRequest("/")); + const response2 = await router(createRequest("/" + id)); assertEquals(await response1.text(), "not found"); assertEquals(await response2.text(), id); }); Deno.test("rejects if no match", async () => { - const router = new HttpRouter(); + const router = createRouter(); router.get("/", () => new Response("ok")); - await assertRejects(() => router.handleRequest(createRequest("/abc"))); + await assertRejects(() => router(createRequest("/abc"))); }); Deno.test("can use fallback if no match", async () => { - const router = new HttpRouter(); + const router = createRouter(); router.get("/", () => new Response("ok")); router.get("/abcd", () => new Response("ok")); router.all("*", () => new Response("no match")); - const response = await router.handleRequest(createRequest("/abc")); + const response = await router(createRequest("/abc")); assertEquals(await response.text(), "no match"); });