Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(http/unstable): route module #5644

Merged
merged 18 commits into from
Aug 7, 2024
3 changes: 2 additions & 1 deletion http/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"./server-sent-event-stream": "./server_sent_event_stream.ts",
"./status": "./status.ts",
"./signed-cookie": "./signed_cookie.ts",
"./user-agent": "./user_agent.ts"
"./user-agent": "./user_agent.ts",
"./route": "./route.ts"
}
}
32 changes: 32 additions & 0 deletions http/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,37 @@
* });
* ```
*
* ### Routing
*
* {@linkcode route} provides an easy way to route requests to different
* handlers based on the request path and method.
*
* ```ts no-eval
* import { route, type Route } from "@std/http/route";
* import { serveDir } from "@std/http/file-server";
*
* const routes: Route[] = [
* {
* pattern: new URLPattern({ pathname: "/about" }),
* handler: () => new Response("About page"),
* },
* {
* pattern: new URLPattern({ pathname: "/users/:id" }),
* handler: (_req, _info, params) => new Response(params?.pathname.groups.id),
* },
* {
* pattern: new URLPattern({ pathname: "/static/*" }),
* handler: (req: Request) => serveDir(req)
* }
* ];
*
* function defaultHandler(_req: Request) {
* return new Response("Not found", { status: 404 });
* }
*
* Deno.serve(route(routes, defaultHandler));
* ```
*
* @module
*/

Expand All @@ -69,5 +100,6 @@ export * from "./signed_cookie.ts";
export * from "./server_sent_event_stream.ts";
export * from "./user_agent.ts";
export * from "./file_server.ts";
export * from "./route.ts";
export * from "./header.ts";
export * from "./method.ts";
101 changes: 101 additions & 0 deletions http/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

/**
* Request handler for {@linkcode Route}.
*
* > [!WARNING]
* > **UNSTABLE**: New API, yet to be vetted.
*
* @experimental
*
* Extends {@linkcode Deno.ServeHandlerInfo} by adding adding a `params` argument.
*
* @param request Request
* @param info Request info
* @param params URL pattern result
*/
export type Handler = (
request: Request,
info: Deno.ServeHandlerInfo,
params?: URLPatternResult | null,
) => Response | Promise<Response>;

/**
* Route configuration for {@linkcode route}.
*
* > [!WARNING]
* > **UNSTABLE**: New API, yet to be vetted.
*
* @experimental
*/
export interface Route {
/**
* Request URL pattern.
*/
pattern: URLPattern;
/**
* Request method.
*
* @default {"GET"}
*/
method?: string;
/**
* Request handler.
*/
handler: Handler;
}

/**
* Routes requests to different handlers based on the request path and method.
*
* > [!WARNING]
* > **UNSTABLE**: New API, yet to be vetted.
*
* @experimental
*
* @example Usage
* ```ts no-eval
* import { route, type Route } from "@std/http/route";
* import { serveDir } from "@std/http/file-server";
*
* const routes: Route[] = [
* {
* pattern: new URLPattern({ pathname: "/about" }),
* handler: () => new Response("About page"),
* },
* {
* pattern: new URLPattern({ pathname: "/users/:id" }),
* handler: (_req, _info, params) => new Response(params?.pathname.groups.id),
* },
* {
* pattern: new URLPattern({ pathname: "/static/*" }),
* handler: (req: Request) => serveDir(req)
* }
* ];
*
* function defaultHandler(_req: Request) {
* return new Response("Not found", { status: 404 });
* }
*
* Deno.serve(route(routes, defaultHandler));
* ```
*
* @param routes Route configurations
* @param defaultHandler Default request handler that's returned when no route
* matches the given request. Serving HTTP 404 Not Found or 405 Method Not
* Allowed response can be done in this function.
* @returns Request handler
*/
export function route(
routes: Route[],
defaultHandler: Deno.ServeHandler,
): Deno.ServeHandler {
// TODO(iuioiua): Use `URLPatternList` once available (https://github.com/whatwg/urlpattern/pull/166)
return (request: Request, info: Deno.ServeHandlerInfo) => {
for (const route of routes) {
const match = route.pattern.exec(request.url);
if (match) return route.handler(request, info, match);
}
return defaultHandler(request, info);
};
}
57 changes: 57 additions & 0 deletions http/route_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

import { type Route, route } from "./route.ts";
import { assertEquals } from "../assert/equals.ts";

const routes: Route[] = [
{
pattern: new URLPattern({ pathname: "/about" }),
handler: (request: Request) => new Response(new URL(request.url).pathname),
},
{
pattern: new URLPattern({ pathname: "/users/:id" }),
method: "POST",
handler: (_request, _info, params) =>
new Response(params?.pathname.groups.id),
},
];

function defaultHandler(request: Request) {
return new Response(new URL(request.url).pathname, { status: 404 });
}

const info: Deno.ServeHandlerInfo = {
remoteAddr: {
transport: "tcp",
hostname: "example.com",
port: 80,
},
completed: Promise.resolve(),
};

Deno.test("route()", async (t) => {
const handler = route(routes, defaultHandler);

await t.step("handles static routes", async () => {
const request = new Request("http://example.com/about");
const response = await handler(request, info);
assertEquals(response?.status, 200);
assertEquals(await response?.text(), "/about");
});

await t.step("handles dynamic routes", async () => {
const request = new Request("http://example.com/users/123", {
method: "POST",
});
const response = await handler(request, info);
assertEquals(await response?.text(), "123");
assertEquals(response?.status, 200);
});

await t.step("handles default handler", async () => {
const request = new Request("http://example.com/not-found");
const response = await handler(request, info);
assertEquals(response?.status, 404);
assertEquals(await response?.text(), "/not-found");
});
});