Compare commits

...

13 Commits

Author SHA1 Message Date
0aecd354c7 tests passing 2025-07-03 01:48:25 -06:00
0b4f504ba2 first iteration of v.3 router 2025-07-02 23:40:16 -06:00
1d3be78917 improved content types 2025-01-21 01:23:41 -07:00
fcba2014c2 file server content type headers 2025-01-20 23:45:07 -07:00
3b1a969145 hack workaround that might not even work, thanks jsr for being unusable 2025-01-20 21:16:19 -07:00
5de39d8573 resolves errors in filerouter dynamic import 2025-01-20 20:56:30 -07:00
fa73b0bdc0 Even more docs, adds response utils to module 2025-01-04 19:27:29 -07:00
5dad5bc0b1 did more docs 2025-01-04 18:38:54 -07:00
0c8d1865ff prep for publish 2025-01-04 18:05:31 -07:00
b73af68989 Implements basic file based router 2025-01-02 16:06:55 -07:00
defd40293f mod docs 2024-12-01 05:31:21 -07:00
3e68ca285b exports types 2024-12-01 03:57:56 -07:00
67ddb71fdd fixes handlers not being compatible with deno handlers 2024-11-11 12:41:56 -07:00
15 changed files with 1353 additions and 139 deletions

View File

@@ -7,24 +7,26 @@ A simple router for Deno.
## Usage
### Basics
```ts
import Router from '@bearmetal/router';
import Router from "@bearmetal/router";
const router = new Router();
router
.route('/users')
.get((ctx) => {
return new Response('GET /users');
.route("/users")
.get((req, ctx) => {
return new Response("GET /users");
})
.post((ctx) => {
return new Response('POST /users');
.post((req, ctx) => {
return new Response("POST /users");
});
Deno.serve(router.handle)
Deno.serve(router.handle);
```
### Middleware
```ts
...
@@ -37,6 +39,7 @@ router.use('/users', async (ctx, next) => {
```
### Nested Routers
```ts
...
@@ -44,10 +47,10 @@ const nestedRouter = new Router();
nestedRouter
.route('/users')
.get((ctx) => {
.get((req, ctx) => {
return new Response('GET /users');
})
.post((ctx) => {
.post((req, ctx) => {
return new Response('POST /users');
});
@@ -56,3 +59,50 @@ router.use('/users', nestedRouter);
...
```
### Static Files
```ts
...
router.serveDirectory('dirname', '/url-root') // files from 'dirname' directory will be available at '/url-root/filename'
// To automatically locate index.html pages:
router.serveDirectory('dirWithIndexHtml', '/indexes', {showIndex: true});
// Will also generate an index if there is no index.html present in the directory
...
```
### File-based Routing
**Note:** _This is an experimental feature and may change in the future.
Currently, JSR does not support dynamic imports for external modules in Deno. In
order to use this feature, you will need to install as an HTTP module (available
soon)._
```ts
import { FileRouter } from "@bearmetal/router";
const router = new FileRouter("dirname");
Deno.listen(router.handle);
// dirname/index.ts - will be accessible at '/'
export default function (req, ctx) {
return new Response("Hello, world!");
}
// dirname/methods.ts - will be accessible at '/methods'
export const handlers = {
get() {
return new Response("Hello, world");
},
post(req, ctx) {
const data = doDataOp(req.body);
return new Response(data);
},
};
// dirname/nestedRouter.ts - will be accessible at '/nestedRouter'
import { router } from "@bearmetal/router";
const router = new Router();
export default router;
```

View File

@@ -1,15 +1,21 @@
{
"name": "@bearmetal/router",
"description": "A simple router for Deno",
"version": "0.1.0",
"version": "0.2.5",
"stable": true,
"repository": "https://github.com/emmaos/bearmetal",
"files": [
"mod.ts",
"README.md",
"LICENSE"
],
"exports": "./mod.ts",
"exports": {
".": "./mod.ts",
"./types": "./types.ts",
"./util/response": "./util/response.ts"
},
"exclude": [
".vscode/"
],
"imports": {
"@std/assert": "jsr:@std/assert@^1.0.7",
"@std/testing": "jsr:@std/testing@^1.0.4"

7
deno.lock generated
View File

@@ -1,7 +1,8 @@
{
"version": "4",
"version": "5",
"specifiers": {
"jsr:@std/assert@^1.0.7": "1.0.7",
"jsr:@std/async@^1.0.8": "1.0.13",
"jsr:@std/data-structures@^1.0.4": "1.0.4",
"jsr:@std/fs@^1.0.5": "1.0.5",
"jsr:@std/internal@^1.0.5": "1.0.5",
@@ -16,6 +17,9 @@
"jsr:@std/internal"
]
},
"@std/async@1.0.13": {
"integrity": "1d76ca5d324aef249908f7f7fe0d39aaf53198e5420604a59ab5c035adc97c96"
},
"@std/data-structures@1.0.4": {
"integrity": "fa0e20c11eb9ba673417450915c750a0001405a784e2a4e0c3725031681684a0"
},
@@ -35,6 +39,7 @@
"integrity": "ca1368d720b183f572d40c469bb9faf09643ddd77b54f8b44d36ae6b94940576",
"dependencies": [
"jsr:@std/assert",
"jsr:@std/async",
"jsr:@std/data-structures",
"jsr:@std/fs",
"jsr:@std/internal",

7
fileRouterTest/test.ts Normal file
View File

@@ -0,0 +1,7 @@
const handlers = {
get: () => {
return new Response("Hello World");
},
};
export default handlers;

85
file_router.ts Normal file
View File

@@ -0,0 +1,85 @@
import Router from "./router.ts";
import type { Handler, RouteConfigurator } from "./types.ts";
import { isRelativePath } from "./util/isRelativePath.ts";
function crawl(dir: string, callback: (path: string) => void) {
for (const entry of Deno.readDirSync(dir)) {
const path = Deno.build.os === "windows"
? `${dir}\\${entry.name}`
: `${dir}/${entry.name}`;
if (entry.isDirectory) {
crawl(path, callback);
} else {
callback(path);
}
}
}
/**
* @example
* ```ts
* import {FileRouter} from "@bearmetal/router"
*
* const router = new FileRouter("dirName");
* Deno.listen(router.handle);
* ```
*/
export class FileRouter extends Router {
constructor(root: string, _import?: (path: string) => Promise<unknown>) {
super();
crawl(root, async (path) => {
let relativePath = path.replace(root, "").replace(/index\/?/, "");
if (path.endsWith(".ts") || path.endsWith(".js")) {
if (isRelativePath(path)) {
path = Deno.build.os === "windows"
? `${Deno.cwd()}\\${path.replace(/^.?.?\\/, "")}`
: `${Deno.cwd()}/${path.replace(/^.?.?\//, "")}`;
}
relativePath = relativePath.replace(/\.[tj]s/, "");
const handlers = _import
? await _import(path)
: await import("file://" + path);
if (handlers.default) {
handlers.default instanceof Router
? this.use(relativePath, handlers.default)
: this.route(relativePath).get(handlers.default);
}
if (handlers.handlers) {
for (const [method, handler] of Object.entries(handlers.handlers)) {
this.route(relativePath)[method as keyof RouteConfigurator](
handler as Handler,
);
}
}
}
if (path.endsWith(".htm") || path.endsWith(".html")) {
const headers = new Headers();
headers.set("Content-Type", "text/html");
this.route(relativePath).get((_ctx) => {
return new Response(Deno.readTextFileSync(path), { headers });
});
}
});
if (Deno.env.get("BEARMETAL_ROUTER_DEBUG") === "true") {
this.route("/debug/dump").get((_ctx) => {
console.log("Dumping routes:");
return new Response(
this.routes.map((r) => r.pattern.pathname).join("\n"),
);
});
}
}
}
if (import.meta.main) {
const router = new FileRouter("fileRouterTest");
router.serveDirectory("docs", "/", { showIndex: true });
Deno.serve({
port: 8000,
handler: router.handle,
});
}

8
mod.ts
View File

@@ -1 +1,7 @@
export { Router } from "./router.ts";
/**
* @module
* BearMetal Router, for routing HTTP requests with Deno.serve
*/
export { Router } from "./router.ts";
export { FileRouter } from "./file_router.ts";

302
router.new.test.ts Normal file
View File

@@ -0,0 +1,302 @@
// deno-lint-ignore-file require-await
import { assertEquals } from "@std/assert";
import { beforeEach, describe, it } from "@std/testing/bdd";
import Router from "./router.new.ts";
describe("Router", () => {
let router: Router;
beforeEach(() => {
router = new Router();
});
describe("Basic Routing", () => {
it("should handle basic GET request", async () => {
router.route("/test")
.get(async () => new Response("test response"));
const req = new Request("http://localhost/test", {
method: "GET",
});
const res = await router.handle(req);
assertEquals(res.status, 200);
assertEquals(await res.text(), "test response");
});
it("should return 404 for non-existent route", async () => {
const req = new Request("http://localhost/nonexistent", {
method: "GET",
});
const res = await router.handle(req);
assertEquals(res.status, 404);
});
it("should return 405 for unsupported method", async () => {
router.route("/test")
.get(async () => new Response("test"));
const req = new Request("http://localhost/test", {
method: "POST",
});
const res = await router.handle(req);
assertEquals(res.status, 405);
});
});
describe("Route Parameters", () => {
it("should handle route parameters", async () => {
router.route("/users/:id")
.get(async (_, ctx) => new Response(ctx.params.id));
const req = new Request("http://localhost/users/123", {
method: "GET",
});
const res = await router.handle(req);
assertEquals(await res.text(), "123");
});
it("should handle multiple route parameters", async () => {
router.route("/users/:userId/posts/:postId")
.get(async (_, ctx) => {
return new Response(JSON.stringify(ctx.params));
});
const req = new Request("http://localhost/users/123/posts/456", {
method: "GET",
});
const res = await router.handle(req);
const params = JSON.parse(await res.text());
assertEquals(params.userId, "123");
assertEquals(params.postId, "456");
});
});
describe("Middleware", () => {
it("should execute global middleware", async () => {
let middlewareExecuted = false;
router.use(async (_, _ctx, next) => {
middlewareExecuted = true;
return await next();
});
router.route("/test")
.get(async () => new Response("test"));
const req = new Request("http://localhost/test", {
method: "GET",
});
await router.handle(req);
assertEquals(middlewareExecuted, true);
});
it("should execute path-specific middleware", async () => {
let middlewareExecuted = false;
router.use("/test", async (_, _ctx, next) => {
middlewareExecuted = true;
return await next();
});
router.route("/test")
.get(async () => new Response("test"));
const req = new Request("http://localhost/test", {
method: "GET",
});
await router.handle(req);
assertEquals(middlewareExecuted, true);
});
it("should handle middleware parameters", async () => {
const capturedParams: Record<string, string | undefined> = {};
router.use("/:version/.*", async (_, ctx, next) => {
capturedParams.version = ctx.params.version;
return await next();
});
router.route("/:version/test")
.get(async () => new Response("test"));
const req = new Request("http://localhost/v1/test", {
method: "GET",
});
await router.handle(req);
assertEquals(capturedParams.version, "v1");
});
it("should execute middleware in correct order", async () => {
const order: number[] = [];
router.use(async (_, _ctx, next) => {
order.push(1);
return await next();
});
router.use("/test", async (_, _ctx, next) => {
order.push(2);
return await next();
});
router.route("/test")
.get(async () => {
order.push(3);
return new Response("test");
});
const req = new Request("http://localhost/test", {
method: "GET",
});
await router.handle(req);
assertEquals(order, [1, 2, 3]);
});
});
describe("Nested Routers", () => {
it("should handle nested routes", async () => {
const apiRouter = new Router();
apiRouter.route("/test")
.get(async () => new Response("api test"));
router.use("/api", apiRouter);
const req = new Request("http://localhost/api/test", {
method: "GET",
});
const res = await router.handle(req);
assertEquals(await res.text(), "api test");
});
it("should handle nested routes with parameters", async () => {
const apiRouter = new Router();
apiRouter.route("/users/:id")
.get(async (_, ctx) => new Response(ctx.params.id));
router.use("/api/:version", apiRouter);
const req = new Request("http://localhost/api/v1/users/123", {
method: "GET",
});
const res = await router.handle(req);
assertEquals(await res.text(), "123");
});
it("should handle nested middleware", async () => {
const apiRouter = new Router();
let middlewareExecuted = false;
apiRouter.use(async (_, _ctx, next) => {
middlewareExecuted = true;
return await next();
});
apiRouter.route("/test")
.get(async () => new Response("test"));
router.use("/api", apiRouter);
const req = new Request("http://localhost/api/test", {
method: "GET",
});
await router.handle(req);
assertEquals(middlewareExecuted, true);
});
});
describe("Context State", () => {
it("should maintain state across middleware chain", async () => {
router.use(async (_, ctx, next) => {
ctx.state.test = "value";
return await next();
});
router.route("/test")
.get(async (_, ctx) => {
return new Response(ctx.state.test as string);
});
const req = new Request("http://localhost/test", {
method: "GET",
});
const res = await router.handle(req);
assertEquals(await res.text(), "value");
});
});
describe("Error Handling", () => {
it("should handle errors in handlers", async () => {
router.route("/error")
.get(async () => {
throw new Error("Test error");
});
const req = new Request("http://localhost/error", {
method: "GET",
});
const res = await router.handle(req);
assertEquals(res.status, 500);
assertEquals(await res.text(), "Internal Server Error");
});
it("should handle errors in middleware", async () => {
router.use(async () => {
throw new Error("Middleware error");
});
router.route("/test")
.get(async () => new Response("test"));
const req = new Request("http://localhost/test", {
method: "GET",
});
const res = await router.handle(req);
assertEquals(res.status, 500);
});
it("should handle no handlers returning a response", async () => {
router.get("/test", async () => undefined as unknown as Response);
const req = new Request("http://localhost/test", {
method: "GET",
});
const res = await router.handle(req);
assertEquals(res.status, 501);
});
});
describe("HTTP Methods", () => {
const methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"];
methods.forEach((method) => {
it(`should handle ${method} requests`, async () => {
const route = router.route("/test");
route[method.toLowerCase() as keyof typeof route](
async () => new Response(method),
);
const req = new Request("http://localhost/test", {
method: method,
});
const res = await router.handle(req);
assertEquals(res.status, 200);
assertEquals(await res.text(), method);
});
});
});
});

281
router.new.ts Normal file
View File

@@ -0,0 +1,281 @@
import type { RouterContext as oldContext } from "@bearmetal/router/types";
import { joinPaths } from "./util/join.ts";
import { InternalError, NotFound } from "@bearmetal/router/util/response";
type RouterContext = Omit<oldContext, "pattern">;
type Handler = (
req: Request,
ctx: RouterContext,
next: () => Promise<Response>,
) => Promise<Response>;
type RouteConfigurator = {
get(handler: Handler): RouteConfigurator;
post(handler: Handler): RouteConfigurator;
put(handler: Handler): RouteConfigurator;
delete(handler: Handler): RouteConfigurator;
patch(handler: Handler): RouteConfigurator;
options(handler: Handler): RouteConfigurator;
use(handler: Handler | Router): RouteConfigurator;
};
type RouteConfig = {
handlers: { [method: string]: Handler[] };
pattern: URLPattern;
};
const GET = "GET";
const POST = "POST";
const PUT = "PUT";
const PATCH = "PATCH";
const DELETE = "DELETE";
const OPTIONS = "OPTIONS";
const _use = "_use";
const methods = [GET, POST, PUT, PATCH, DELETE, OPTIONS, _use];
export class Router {
protected routes: Map<string, RouteConfig> = new Map();
/**
* @description defines a new route
* @param path the path to match, uses the same syntax as the URLPattern constructor
* @returns a RouteConfigurator object
*
* @example
* ```ts
* router.route('/users')
* .get((req, ctx) => {
* return new Response('GET /users');
* })
* .post((req, ctx) => {
* return new Response('POST /users');
* });
* ```
*/
route(path: string): RouteConfigurator {
path = fixPath(path);
const routeConfig = this.getOrCreateConfig(path);
const configurator = {
get: (handler: Handler) => {
this.getOrCreateConfigHandlers(GET, routeConfig).push(handler);
return configurator;
},
post: (handler: Handler) => {
this.getOrCreateConfigHandlers(POST, routeConfig).push(handler);
return configurator;
},
put: (handler: Handler) => {
this.getOrCreateConfigHandlers(PUT, routeConfig).push(handler);
return configurator;
},
patch: (handler: Handler) => {
this.getOrCreateConfigHandlers(PATCH, routeConfig).push(handler);
return configurator;
},
delete: (handler: Handler) => {
this.getOrCreateConfigHandlers(DELETE, routeConfig).push(handler);
return configurator;
},
options: (handler: Handler) => {
this.getOrCreateConfigHandlers(OPTIONS, routeConfig).push(handler);
return configurator;
},
use: (handler: Handler | Router) => {
if (handler instanceof Router) {
this.resolveRouterHandlerStack(path, handler);
return configurator;
}
this.getOrCreateConfigHandlers(_use, routeConfig).push(handler);
return configurator;
},
};
return configurator;
}
get(handler: Handler): void;
get(path: string, handler: Handler): void;
get(path: string | Handler, handler?: Handler): void {
if (typeof path !== "string") {
handler = path;
path = "/.*";
} else {
path = fixPath(path);
}
const config = this.getOrCreateConfig(path);
this.getOrCreateConfigHandlers(GET, config).push(handler!);
}
post(handler: Handler): void;
post(path: string, handler: Handler): void;
post(path: string | Handler, handler?: Handler): void {
if (typeof path !== "string") {
handler = path;
path = "/.*";
} else {
path = fixPath(path);
}
const config = this.getOrCreateConfig(path);
this.getOrCreateConfigHandlers(POST, config).push(handler!);
}
put(handler: Handler): void;
put(path: string, handler: Handler): void;
put(path: string | Handler, handler?: Handler): void {
if (typeof path !== "string") {
handler = path;
path = "/.*";
} else {
path = fixPath(path);
}
const config = this.getOrCreateConfig(path);
this.getOrCreateConfigHandlers(PUT, config).push(handler!);
}
patch(handler: Handler): void;
patch(path: string, handler: Handler): void;
patch(path: string | Handler, handler?: Handler): void {
if (typeof path !== "string") {
handler = path;
path = "/.*";
} else {
path = fixPath(path);
}
const config = this.getOrCreateConfig(path);
this.getOrCreateConfigHandlers(PATCH, config).push(handler!);
}
delete(handler: Handler): void;
delete(path: string, handler: Handler): void;
delete(path: string | Handler, handler?: Handler): void {
if (typeof path !== "string") {
handler = path;
path = "/.*";
} else {
path = fixPath(path);
}
const config = this.getOrCreateConfig(path);
this.getOrCreateConfigHandlers(DELETE, config).push(handler!);
}
options(handler: Handler): void;
options(path: string, handler: Handler): void;
options(path: string | Handler, handler?: Handler): void {
if (typeof path !== "string") {
handler = path;
path = "/.*";
} else {
path = fixPath(path);
}
const config = this.getOrCreateConfig(path);
this.getOrCreateConfigHandlers(OPTIONS, config).push(handler!);
}
use(handler: Handler | Router): void;
use(path: string, handler: Handler | Router): void;
use(path: string | Handler | Router, handler?: Handler | Router): void {
if (typeof path !== "string") {
handler = path;
path = "/.*";
} else {
path = fixPath(path);
}
if (handler instanceof Router) {
return this.resolveRouterHandlerStack(path, handler);
}
const config = this.getOrCreateConfig(path);
this.getOrCreateConfigHandlers(_use, config).push(handler!);
}
private getOrCreateConfig(path: string): RouteConfig {
let config = this.routes.get(path);
if (!config) {
config = {
handlers: {},
pattern: new URLPattern({ pathname: path }),
};
this.routes.set(path, config);
}
return config;
}
private getOrCreateConfigHandlers(method: string, config: RouteConfig) {
config.handlers[method] = config.handlers[method] ?? [];
return config.handlers[method];
}
resolveRouterHandlerStack(path: string, router: Router): void {
for (const route of router.rawRoutes) {
const p = joinPaths(path, route[0]);
const thisConfig = this.getOrCreateConfig(p);
const thatConfig = route[1];
for (const method of methods) {
if (!thatConfig.handlers[method]) continue;
thisConfig.handlers[method] = (thisConfig.handlers[method] ?? [])
.concat(thatConfig.handlers[method]);
}
}
}
get rawRoutes() {
return this.routes.entries();
}
get handle() {
return this.handler;
}
async handler(req: Request) {
const url = new URL(req.url);
const method = req.method;
const matchingRoutes = this.findMatchingRoutes(url);
if (!matchingRoutes.length) return NotFound();
const matchingMethods = matchingRoutes.some((r) =>
Object.hasOwn(r.config.handlers, method)
);
if (!matchingMethods) {
return new Response("Method Not Allowed", { status: 405 });
}
const middlewareStack = matchingRoutes.flatMap((r) =>
(r.config.handlers[_use] ?? []).concat(r.config.handlers[method] ?? [])
);
const ctx: RouterContext = {
url,
params: matchingRoutes.reduce((a, b) => ({ ...a, ...b.params }), {}),
state: {},
request: req,
};
let index = 0;
const executeMiddleware = async (): Promise<Response> => {
if (index < middlewareStack.length) {
const res = await middlewareStack[index++]?.(
req,
ctx,
executeMiddleware,
);
if (res instanceof Response) return res;
}
return new Response("End of stack", { status: 501 });
};
try {
return await executeMiddleware();
} catch {
return InternalError();
}
}
private findMatchingRoutes(url: URL) {
return this.routes.values().map((route) => {
const result = route.pattern.exec(url);
if (result) {
return {
config: route,
params: result.pathname.groups,
};
}
}).filter((r) => !!r).toArray();
}
}
function fixPath(path: string) {
path = path.startsWith("/") ? path : `/${path}`;
return path;
}
export default Router;

View File

@@ -49,7 +49,7 @@ describe("Router", () => {
describe("Route Parameters", () => {
it("should handle route parameters", async () => {
router.route("/users/:id")
.get(async (ctx) => new Response(ctx.params.id));
.get(async (_, ctx) => new Response(ctx.params.id));
const req = new Request("http://localhost/users/123", {
method: "GET",
@@ -61,7 +61,7 @@ describe("Router", () => {
it("should handle multiple route parameters", async () => {
router.route("/users/:userId/posts/:postId")
.get(async (ctx) => {
.get(async (_, ctx) => {
return new Response(JSON.stringify(ctx.params));
});
@@ -80,7 +80,7 @@ describe("Router", () => {
it("should execute global middleware", async () => {
let middlewareExecuted = false;
router.use(async (_ctx, next) => {
router.use(async (_, _ctx, next) => {
middlewareExecuted = true;
return await next();
});
@@ -99,9 +99,9 @@ describe("Router", () => {
it("should execute path-specific middleware", async () => {
let middlewareExecuted = false;
router.use("/test", async (_ctx, next) => {
router.use("/test", async (_, _ctx, next) => {
middlewareExecuted = true;
console.log('middlware happened')
console.log("middlware happened");
return await next();
});
@@ -119,7 +119,7 @@ describe("Router", () => {
it("should handle middleware parameters", async () => {
const capturedParams: Record<string, string | undefined> = {};
router.use("/:version/.*", async (ctx, next) => {
router.use("/:version/.*", async (_, ctx, next) => {
capturedParams.version = ctx.params.version;
return await next();
});
@@ -138,12 +138,12 @@ describe("Router", () => {
it("should execute middleware in correct order", async () => {
const order: number[] = [];
router.use(async (_ctx, next) => {
router.use(async (_, _ctx, next) => {
order.push(1);
return await next();
});
router.use("/test", async (_ctx, next) => {
router.use("/test", async (_, _ctx, next) => {
order.push(2);
return await next();
});
@@ -182,7 +182,7 @@ describe("Router", () => {
it("should handle nested routes with parameters", async () => {
const apiRouter = new Router();
apiRouter.route("/users/:id")
.get(async (ctx) => new Response(ctx.params.id));
.get(async (_, ctx) => new Response(ctx.params.id));
router.use("/api/:version", apiRouter);
@@ -198,7 +198,7 @@ describe("Router", () => {
const apiRouter = new Router();
let middlewareExecuted = false;
apiRouter.use(async (_ctx, next) => {
apiRouter.use(async (_, _ctx, next) => {
middlewareExecuted = true;
return await next();
});
@@ -219,13 +219,13 @@ describe("Router", () => {
describe("Context State", () => {
it("should maintain state across middleware chain", async () => {
router.use(async (ctx, next) => {
router.use(async (_, ctx, next) => {
ctx.state.test = "value";
return await next();
});
router.route("/test")
.get(async (ctx) => {
.get(async (_, ctx) => {
return new Response(ctx.state.test as string);
});
@@ -274,11 +274,11 @@ describe("Router", () => {
describe("HTTP Methods", () => {
const methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"];
methods.forEach(method => {
methods.forEach((method) => {
it(`should handle ${method} requests`, async () => {
const route = router.route("/test");
route[method.toLowerCase() as keyof typeof route](
async () => new Response(method)
async () => new Response(method),
);
const req = new Request("http://localhost/test", {
@@ -291,4 +291,4 @@ describe("Router", () => {
});
});
});
});
});

564
router.ts
View File

@@ -1,40 +1,19 @@
interface RouterContext {
url: URL;
params: Record<string, string | undefined>;
state: Record<string, unknown>;
pattern: URLPattern;
request: Request;
}
type Handler = (ctx: RouterContext) => Promise<Response> | Response;
type Middleware = (ctx: RouterContext, next: () => Promise<Response>) => Promise<Response>;
interface RouteConfig {
pattern: URLPattern;
handlers: { [method: string]: Handler };
}
interface MiddlewareConfig {
pattern: URLPattern;
handler: Middleware;
// Add path for sorting
path: string;
}
interface RouteConfigurator {
get(handler: Handler): RouteConfigurator;
post(handler: Handler): RouteConfigurator;
put(handler: Handler): RouteConfigurator;
delete(handler: Handler): RouteConfigurator;
patch(handler: Handler): RouteConfigurator;
options(handler: Handler): RouteConfigurator;
}
import type {
Handler,
Middleware,
MiddlewareConfig,
RouteConfig,
RouteConfigurator,
RouterContext,
} from "./types.ts";
import { getContentTypeByExtension } from "./util/contentType.ts";
import { NotFound } from "./util/response.ts";
/**
* A simple router for Deno
*
*
* @author Emmaline Autumn
*
*
* @example
* ```ts
* const router = new Router();
@@ -95,55 +74,88 @@ interface RouteConfigurator {
* });
*/
export class Router {
private routes: RouteConfig[] = [];
protected routes: RouteConfig[] = [];
private middleware: MiddlewareConfig[] = [];
/**
* @description defines a new route
* @param path the path to match, uses the same syntax as the URLPattern constructor
* @returns a RouteConfigurator object
*
* @example
* ```ts
* router.route('/users')
* .get((req, ctx) => {
* return new Response('GET /users');
* })
* .post((req, ctx) => {
* return new Response('POST /users');
* });
* ```
*/
route(path: string): RouteConfigurator {
path = path.startsWith('/') ? path : `/${path}`;
path = path.startsWith("/") ? path : `/${path}`;
const pattern = new URLPattern({ pathname: path });
const routeConfig: RouteConfig = {
pattern,
handlers: {}
handlers: {},
};
this.routes.push(routeConfig);
return {
get(handler: Handler) {
routeConfig.handlers['GET'] = handler;
routeConfig.handlers["GET"] = handler;
return this;
},
post(handler: Handler) {
routeConfig.handlers['POST'] = handler;
routeConfig.handlers["POST"] = handler;
return this;
},
put(handler: Handler) {
routeConfig.handlers['PUT'] = handler;
routeConfig.handlers["PUT"] = handler;
return this;
},
delete(handler: Handler) {
routeConfig.handlers['DELETE'] = handler;
routeConfig.handlers["DELETE"] = handler;
return this;
},
patch(handler: Handler) {
routeConfig.handlers['PATCH'] = handler;
routeConfig.handlers["PATCH"] = handler;
return this;
},
options(handler: Handler) {
routeConfig.handlers['OPTIONS'] = handler;
routeConfig.handlers["OPTIONS"] = handler;
return this;
}
},
};
}
use(pathOrMiddleware: string | Middleware, middlewareOrRouter?: Middleware | Router): Router {
/**
* @description adds a middleware to the router
* @param pathOrMiddleware the path to match, uses the same syntax as the URLPattern constructor
* @param middlewareOrRouter the middleware to add, or a Router object to nest routes
* @returns the Router object
*
* @example
* ```ts
* router.use('/users', async (req, ctx, next) => {
* console.log('Using middleware');
* return await next();
* });
* ```
*/
use(
pathOrMiddleware: string | Middleware,
middlewareOrRouter?: Middleware | Router,
): Router {
// Handle the case where only middleware is provided
if (typeof pathOrMiddleware === 'function') {
const pattern = new URLPattern({ pathname: '/*' });
if (typeof pathOrMiddleware === "function") {
const pattern = new URLPattern({ pathname: "/*" });
this.middleware.push({
pattern,
handler: pathOrMiddleware,
path: '/*'
path: "/*",
});
return this;
}
@@ -152,27 +164,30 @@ export class Router {
const middleware = middlewareOrRouter;
if (!middleware) {
throw new Error('Middleware or Router is required');
throw new Error("Middleware or Router is required");
}
// Normalize the path to handle both exact matches and nested paths
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
const isParameterPath = normalizedPath.includes(':');
const wildcardPath = isParameterPath ?
normalizedPath :
normalizedPath === '/' ?
'/*' :
`${normalizedPath.replace(/\/+$/, '')}/*?`;
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
const isParameterPath = normalizedPath.includes(":");
const wildcardPath = isParameterPath
? normalizedPath
: normalizedPath === "/"
? "/*"
: `${normalizedPath.replace(/\/+$/, "")}/*?`;
if (middleware instanceof Router) {
// Merge the nested router's routes
for (const nestedRoute of middleware.routes) {
const combinedPath = this.combinePaths(path, nestedRoute.pattern.pathname);
const combinedPath = this.combinePaths(
path,
nestedRoute.pattern.pathname,
);
const newPattern = new URLPattern({ pathname: combinedPath });
this.routes.push({
pattern: newPattern,
handlers: { ...nestedRoute.handlers }
handlers: { ...nestedRoute.handlers },
});
}
@@ -180,11 +195,11 @@ export class Router {
for (const nestedMiddleware of middleware.middleware) {
const combinedPath = this.combinePaths(path, nestedMiddleware.path);
const newPattern = new URLPattern({ pathname: combinedPath });
this.middleware.push({
pattern: newPattern,
handler: nestedMiddleware.handler,
path: combinedPath
path: combinedPath,
});
}
} else {
@@ -193,103 +208,113 @@ export class Router {
this.middleware.push({
pattern,
handler: middleware,
path: wildcardPath
path: wildcardPath,
});
}
return this;
}
/**
* @description handles incoming requests
* @param req the incoming request
* @returns a Response object
*
* @example
* ```ts
* Deno.serve({
* port: 8000,
* handler: router.handle
* });
* ```
*/
handle = async (req: Request): Promise<Response> => {
const url = new URL(req.url);
const method = req.method;
// Find the matching route
const matchedRoute = this.findMatchingRoute(url);
if (!matchedRoute) {
return new Response('Not Found', { status: 404 });
return new Response("Not Found", { status: 404 });
}
const handler = matchedRoute.config.handlers[method];
if (!handler) {
return new Response('Method Not Allowed', { status: 405 });
return new Response("Method Not Allowed", { status: 405 });
}
// Get matching middleware and sort by path specificity
const matchingMiddleware = this.getMatchingMiddleware(url);
// Create the base context object
const baseCtx: RouterContext = {
url,
params: {},
state: {},
pattern: matchedRoute.config.pattern,
request: req
request: req,
};
// Combine route parameters with the base context
baseCtx.params = {
...baseCtx.params,
...matchedRoute.params
...matchedRoute.params,
};
// Execute middleware chain
let index = 0;
const executeMiddleware = async (): Promise<Response> => {
if (index < matchingMiddleware.length) {
const { middleware, params } = matchingMiddleware[index++];
// Create a new context for each middleware with its specific parameters
const middlewareCtx: RouterContext = {
...baseCtx,
params: { ...baseCtx.params, ...params },
pattern: middleware.pattern
pattern: middleware.pattern,
};
return await middleware.handler(middlewareCtx, executeMiddleware);
return await middleware.handler(req, middlewareCtx, executeMiddleware);
}
// Final handler gets the accumulated parameters
return await handler(baseCtx);
return await handler(req, baseCtx);
};
try {
return await executeMiddleware();
} catch (error) {
console.error('Error handling request:', error);
return new Response('Internal Server Error', { status: 500 });
console.error("Error handling request:", error);
return new Response("Internal Server Error", { status: 500 });
}
}
};
private findMatchingRoute(url: URL): { config: RouteConfig; params: Record<string, string | undefined> } | null {
private findMatchingRoute(
url: URL,
):
| { config: RouteConfig; params: Record<string, string | undefined> }
| null {
for (const route of this.routes) {
const result = route.pattern.exec(url);
if (result) {
return {
config: route,
params: result.pathname.groups
params: result.pathname.groups,
};
}
}
return null;
}
private getMatchingMiddleware(url: URL): Array<{
middleware: MiddlewareConfig;
params: Record<string, string>
private getMatchingMiddleware(url: URL): Array<{
middleware: MiddlewareConfig;
params: Record<string, string>;
}> {
const matches = this.middleware
.map(mw => {
const result = mw.pattern.exec(url) ?? mw.pattern.exec(url.href + '/');
.map((mw) => {
const result = mw.pattern.exec(url) ?? mw.pattern.exec(url.href + "/");
// console.log(url, mw.pattern, result);
if (result) {
return {
middleware: mw,
params: result.pathname.groups
params: result.pathname.groups,
};
}
return null;
})
.filter((item): item is {
middleware: MiddlewareConfig;
params: Record<string, string>
.filter((item): item is {
middleware: MiddlewareConfig;
params: Record<string, string>;
} => item !== null);
// Sort middleware by path specificity
@@ -298,13 +323,13 @@ export class Router {
const pathB = b.middleware.path;
// Global middleware comes first
if (pathA === '/*' && pathB !== '/*') return -1;
if (pathB === '/*' && pathA !== '/*') return 1;
if (pathA === "/*" && pathB !== "/*") return -1;
if (pathB === "/*" && pathA !== "/*") return 1;
// More specific paths (with more segments) come later
const segmentsA = pathA.split('/').length;
const segmentsB = pathB.split('/').length;
const segmentsA = pathA.split("/").length;
const segmentsB = pathB.split("/").length;
if (segmentsA !== segmentsB) {
return segmentsA - segmentsB;
}
@@ -315,24 +340,343 @@ export class Router {
}
private combinePaths(basePath: string, routePath: string): string {
const normalizedBase = basePath.replace(/\/+$/, '').replace(/^\/*/, '/');
const normalizedRoute = routePath.replace(/^\/*/, '');
return normalizedBase === '/' ? `/${normalizedRoute}` : `${normalizedBase}/${normalizedRoute}`;
const normalizedBase = basePath.replace(/\/+$/, "").replace(/^\/*/, "/");
const normalizedRoute = routePath.replace(/^\/*/, "");
return normalizedBase === "/"
? `/${normalizedRoute}`
: `${normalizedBase}/${normalizedRoute}`;
}
/**
* @description serves a directory as a static website
* @param dir the directory to serve
* @param root the root path to serve the directory from
* @param opts optional options
* @returns the Router object
*
* @example
* ```ts
* router.serveDirectory('/public', './public');
* ```
*/
serveDirectory(dir: string, root: string, opts?: { showIndex: boolean }) {
this.route(root + "*").get(async (_req, ctx) => {
const { showIndex } = opts ?? { showIndex: false };
let normalizedPath = (dir + "/" +
ctx.url.pathname.replace(new RegExp("^" + root), "")).trim().replace(
"//",
"/",
);
normalizedPath = normalizedPath.replace(
/\/\s?$/,
"",
);
let fileInfo: Deno.FileInfo;
try {
fileInfo = await Deno.stat(normalizedPath);
} catch (error) {
if (error instanceof Deno.errors.NotFound) {
return NotFound();
} else {
throw error;
}
}
if (fileInfo.isDirectory) {
if (!showIndex) return NotFound();
normalizedPath += "/index.html";
}
try {
const file = await Deno.readFile(normalizedPath);
const filetype = normalizedPath.split(".").at(-1);
const contentType = getContentTypeByExtension(filetype);
return new Response(file, { headers: { "Content-Type": contentType } });
} catch (e) {
if (e instanceof Deno.errors.NotFound) {
return showIndex ? generateIndex(normalizedPath) : NotFound();
}
throw e;
}
});
}
}
async function generateIndex(dir: string) {
dir = dir.replace(/index\.html$/, "");
const items: Deno.DirEntry[] = [];
for await (const entry of Deno.readDir(dir)) {
items.push(entry);
}
const fileIcon = `
<svg
width="48"
height="48"
viewBox="0 0 12.7 12.7"
version="1.1"
id="svg1"
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
sodipodi:docname="file icon.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:document-units="mm"
inkscape:zoom="17.318182"
inkscape:cx="10.018373"
inkscape:cy="28.207349"
inkscape:window-width="3440"
inkscape:window-height="1417"
inkscape:window-x="-8"
inkscape:window-y="1432"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
d="m 3.5196818,1.7373617 c -0.3351528,0 -0.605131,0.2694614 -0.605131,0.6046142 v 8.0160481 c 0,0.335153 0.2699782,0.604614 0.605131,0.604614 h 5.6606364 c 0.3351528,0 0.605131,-0.269461 0.605131,-0.604614 V 3.7372396 L 7.7850545,1.7373617 Z"
style="fill:#4d1f6d;stroke-width:0.264583"
id="path4" />
<path
d="M 9.6529679,3.8697209 7.6525732,1.869843 v 1.3952637 c 0,0.3351528 0.2699782,0.6046142 0.605131,0.6046142 z"
style="fill:#918b93;fill-opacity:1;stroke:#4d1f6d;stroke-width:0.265;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
id="path5" />
<rect
style="fill:#918b93;fill-opacity:1;stroke:none;stroke-width:0.285269;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
id="rect5"
width="5.5833006"
height="0.5505569"
x="3.5583498"
y="4.0022206" />
<rect
style="fill:#918b93;fill-opacity:1;stroke:none;stroke-width:0.272493;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
id="rect6"
width="5.0944114"
height="0.5505569"
x="3.5583498"
y="4.8455539" />
<rect
style="fill:#463e4b;fill-opacity:1;stroke:none;stroke-width:0.279751;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
id="rect7"
width="5.3694115"
height="0.5505569"
x="3.5583498"
y="5.682776" />
<rect
style="fill:#918b93;fill-opacity:1;stroke:none;stroke-width:0.279751;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
id="rect8"
width="5.3694115"
height="0.5505569"
x="3.5583498"
y="5.6888871" />
<rect
style="fill:#918b93;fill-opacity:1;stroke:none;stroke-width:0.285269;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
id="rect9"
width="5.5833006"
height="0.5505569"
x="3.5583498"
y="6.5322208" />
<rect
style="fill:#918b93;fill-opacity:1;stroke:none;stroke-width:0.272493;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
id="rect10"
width="5.0944114"
height="0.5505569"
x="3.5583498"
y="7.3755541" />
<rect
style="fill:#918b93;fill-opacity:1;stroke:none;stroke-width:0.279751;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
id="rect11"
width="5.3694115"
height="0.5505569"
x="3.5583498"
y="8.2188873" />
<rect
style="fill:#918b93;fill-opacity:1;stroke:none;stroke-width:0.217416;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
id="rect12"
width="3.2431545"
height="0.5505569"
x="5.8984962"
y="9.7313871" />
</g>
</svg>
`;
const folderIcon = `
<svg
width="48"
height="48"
viewBox="0 0 12.7 12.7"
version="1.1"
id="svg1"
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
sodipodi:docname="folder icon.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:document-units="mm"
inkscape:zoom="12.245804"
inkscape:cx="17.924507"
inkscape:cy="30.990207"
inkscape:window-width="3440"
inkscape:window-height="1417"
inkscape:window-x="-8"
inkscape:window-y="1432"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#463e4b;fill-opacity:1;stroke-width:0.264583"
id="rect3"
width="10.867838"
height="7.4972959"
x="0.91608107"
y="2.1476252"
ry="1.1883322" />
<rect
style="fill:#4d1f6d;fill-opacity:1;stroke-width:0.264583"
id="rect1"
width="10.867838"
height="7.4972959"
x="0.91608107"
y="2.601352"
ry="1.1883322" />
<rect
style="fill:#4d1f6d;fill-opacity:1;stroke-width:0.264583"
id="rect2"
width="4.2779961"
height="4.0619354"
x="0.91608107"
y="1.5426561"
ry="1.1883322" />
</g>
</svg>
`;
const template = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${dir}</title>
<style>
* {
font-family: sans-serif;
}
ul {
margin: 0;
padding: 0;
}
li {
list-style: none;
display: flex;
align-items: center;
gap: 1rem;
padding: 4px 2px;
&:nth-child(odd) {
background-color: rgba(0,0,0, .2)
}
& svg {
height: 1.5rem;
}
}
a {
color: white;
}
@media (prefers-color-scheme: dark) {
:root {
background-color: #1b1220;
color: #dcdcdc
}
ul {
background-color: rgba(255,255,255,.1)
}
}
</style>
</head>
<body>
<h1>${dir}</h1>
<ul>
${
items
.sort((a, b) => {
if (a.isDirectory && b.isDirectory) return a.name > b.name ? 1 : -1;
if (a.isDirectory) return -1;
if (b.isDirectory) return 1;
return a.name > b.name ? 1 : -1;
})
.map((e) =>
`<li>${e.isFile ? fileIcon : folderIcon}<a href="${
"/" + dir.replace(/^\//, "") + e.name
}">${e.name}</a></li>`
)
.join("\n") || "<li>Directory is empty</li>"
}
</ul>
</body>
</html>
`;
return new Response(template, {
headers: {
"Content-Type": "text/html",
},
});
}
export default Router;
if (import.meta.main) {
console.log('Starting server...');
console.log("Starting server...");
const router = new Router();
router.route('/users')
.get((ctx) => {
return new Response('GET /users');
})
// .post((ctx) => {
// return new Response('POST /users');
// });
router.route("/users")
.get((_ctx) => {
return new Response("GET /users");
});
// .post((ctx) => {
// return new Response('POST /users');
// });
// router.route('/users/:id')
// .get((ctx) => {
@@ -372,14 +716,14 @@ if (import.meta.main) {
// return new Response('POST /*');
// });
router.use('/users', async (_, next) => {
console.log('Using middleware');
router.use("/users", async (_req, _ctx, next) => {
console.log("Using middleware");
return await next();
});
Deno.serve({
port: 8000,
handler: router.handle
handler: router.handle,
});
}

60
types.ts Normal file
View File

@@ -0,0 +1,60 @@
/**
* @module
* BearMetal Router types
*/
/**
* @description a context object for a request
*/
export interface RouterContext {
url: URL;
params: Record<string, string | undefined>;
state: Record<string, unknown>;
pattern: URLPattern;
request: Request;
}
/**
* @description a function that handles incoming requests
*/
export type Handler = (
req: Request,
ctx: RouterContext,
) => Promise<Response> | Response;
/**
* @description a middleware function
*/
export type Middleware = (
req: Request,
ctx: RouterContext,
next: () => Promise<Response>,
) => Promise<Response>;
/**
* @description a route configuration
*/
export interface RouteConfig {
pattern: URLPattern;
handlers: { [method: string]: Handler };
}
/**
* @description a middleware configuration
*/
export interface MiddlewareConfig {
pattern: URLPattern;
handler: Middleware;
path: string;
}
/**
* @description a route configurator
*/
export interface RouteConfigurator {
get(handler: Handler): RouteConfigurator;
post(handler: Handler): RouteConfigurator;
put(handler: Handler): RouteConfigurator;
delete(handler: Handler): RouteConfigurator;
patch(handler: Handler): RouteConfigurator;
options(handler: Handler): RouteConfigurator;
}

26
util/contentType.ts Normal file
View File

@@ -0,0 +1,26 @@
export function getContentTypeByExtension(extension?: string) {
switch (extension) {
case "html":
case "htm":
return "text/html";
case "css":
return "text/css";
case "js":
return "text/javascript";
case "json":
return "application/json";
case "png":
return "image/png";
case "jpg":
case "jpeg":
return "image/jpeg";
case "gif":
return "image/gif";
case "svg":
return "image/svg+xml";
case "txt":
return "text/plain";
default:
return "application/octet-stream";
}
}

4
util/isRelativePath.ts Normal file
View File

@@ -0,0 +1,4 @@
export function isRelativePath(path: string) {
return !path.startsWith("/") &&
(path.startsWith("./") || path.startsWith("../"));
}

3
util/join.ts Normal file
View File

@@ -0,0 +1,3 @@
export const joinPaths = (...args: string[]) =>
args.map((a, i, l) => i === l.length - 1 ? a : a.replace(/\/?\.?\*?$/, ""))
.join("");

35
util/response.ts Normal file
View File

@@ -0,0 +1,35 @@
/**
* @module
* BearMetal Router response utilities
*/
/**
* @description a response with a status of 404
*/
export const NotFound = (msg?: string): Response =>
new Response(msg ?? "Not Found", { status: 404 });
/**
* @description a response with a status of 500
*/
export const InternalError = (msg?: string): Response =>
new Response(msg ?? "Internal Server Error", { status: 500 });
/**
* @description a response with a status of 400
*/
export const BadRequest = (msg?: string): Response =>
new Response(msg ?? "Bad Request", { status: 400 });
/**
* @description a response with a status of 401
*/
export const Unauthorized = (msg?: string): Response =>
new Response(msg ?? "Unauthorized", { status: 401 });
/**
* @description a response with a status of 403
*/
export const Forbidden = (msg?: string): Response =>
new Response(msg ?? "Forbidden", { status: 403 });
/**
* @description a response with a status of 200
*/
export const Ok = (msg?: string): Response =>
new Response(msg ?? "OK", { status: 200 });