Skip to content

Instantly share code, notes, and snippets.

@KartikBazzad
Created August 13, 2023 07:27
Show Gist options
  • Save KartikBazzad/4abe3c9a8c57f25d495fa9c808237c08 to your computer and use it in GitHub Desktop.
Save KartikBazzad/4abe3c9a8c57f25d495fa9c808237c08 to your computer and use it in GitHub Desktop.
Simple Express similar API Router For ASTRO with middleware support
// Router class
import type { APIContext } from "astro";
type Method = "GET" | "POST" | "PUT" | "DELETE";
type Route = {
method: Method;
uri: string;
callbacks: Array<(context?: APIContext) => any>;
};
export class Router {
routes: Array<Route> = [];
middlewares: Set<(context: APIContext) => any> = new Set();
constructor() {
this.routes = [];
this.middlewares = new Set();
return this;
}
private addRoute(
method: Method,
uri: string,
...handlers: Array<(context?: any) => any>
) {
if (!uri || !handlers.length)
throw new Error("uri or callback must be given");
if (typeof uri !== "string")
throw new TypeError("typeof uri must be a string");
if (handlers.every((h) => typeof h !== "function"))
throw new TypeError("typeof callback must be a function");
const _route = this.routes.find(
(i) => i.uri === uri && i.method === method
);
if (_route) {
throw new Error(`the uri ${uri} with ${method} already exists`);
}
this.routes.push({ method, uri, callbacks: handlers });
return this;
}
route(path: string, router: Router) {
router.routes.forEach(({ callbacks, method, uri }) => {
this.addRoute(method, path + uri, ...callbacks);
});
return this;
}
use(mw: (ctx: APIContext) => any) {
this.middlewares.add(mw);
}
get(uri: string, ...handlers: Array<(context?: any) => any>) {
this.addRoute("GET", uri, ...handlers);
return this;
}
post(uri: string, ...handlers: Array<(context?: any) => any>) {
this.addRoute("POST", uri, ...handlers);
return this;
}
put(uri: string, ...handlers: Array<(context?: any) => any>) {
this.addRoute("PUT", uri, ...handlers);
return this;
}
delete(uri: string, ...handlers: Array<(context?: any) => any>) {
this.addRoute("DELETE", uri, ...handlers);
return this;
}
private async handler(route: Route, context: APIContext) {
for (const middleware of this.middlewares) {
const response = await middleware.call(this, context);
if (response instanceof Response) {
return response;
}
}
// Run middlewares specific to this route
for (const callback of route.callbacks) {
const response = await callback.call(this, context);
if (response instanceof Response) {
return response;
}
}
return route.callbacks.at(-1)?.call(this, context);
}
async init(context: APIContext) {
const path = context.url.pathname;
const route = this.routes.find((_route) => {
return path.includes(_route.uri);
});
if (route) {
if (route.method.toLowerCase() === context.request.method.toLowerCase()) {
return await this.handler(route, context);
} else {
return new Response("Not Found, 404", { status: 200 });
}
}
return new Response("Not Found, 404", { status: 200 });
}
}
<------------>
//[...path].ts
import { testRouter } from "@/router/route";
import { Router } from "@/router/router";
import type { APIContext, APIRoute } from "astro";
const router = new Router();
router.use((ctx) => {
console.log("middleware");
return new Response("unauthroized", { status: 500 });
});
router.get(
"/hello",
() => {
console.log("Route Middleware");
},
({ url }: APIContext) => {
return new Response("Hello World", { status: 200 });
}
);
router.route("/test-router", testRouter);
router.get("/nice", () => {
return new Response("Yes", { status: 200 });
});
export const all: APIRoute = async (ctx) => await router.init(ctx);
@KartikBazzad
Copy link
Author

Replace trie router with path-to-regex

import type { APIContext } from "astro";
import { pathToRegexp, Key } from "path-to-regexp";

type Method = "GET" | "POST" | "PUT" | "DELETE";
type Route = {
 method: Method;
 uri: string;
 regex: RegExp;
 keys: Key[];
 callbacks: Array<(context?: APIContext) => any>;
};

// class TrieNode {
//   children: Record<string, TrieNode> = {};
//   route: Route | null = null;
// }

export default class Router {
 routes: Array<Route> = [];
 middlewares: Set<(context: APIContext) => any> = new Set();
 basePath: string = "/";
 constructor() {
   this.routes = [];
   this.middlewares = new Set();
   return this;
 }

 setBasePath(path: string) {
   this.basePath = path;
 }

 private addRoute(
   method: Method,
   uri: string,
   ...handlers: Array<(context?: any) => any>
 ) {
   if (!uri || !handlers.length)
     throw new Error("uri or callback must be given");
   if (typeof uri !== "string")
     throw new TypeError("typeof uri must be a string");
   if (handlers.every((h) => typeof h !== "function"))
     throw new TypeError("typeof callback must be a function");

   const keys: Key[] = [];
   const fullPath = this.basePath + uri; // Prepend the base path
   const regex = pathToRegexp(fullPath, keys);
   const route = { method, uri: fullPath, regex, keys, callbacks: handlers };
   this.routes.push(route);

   return this;
 }

 route(path: string, router: Router) {
   router.routes.forEach(({ callbacks, method, uri }) => {
     this.addRoute(method, path + uri, ...callbacks);
   });
   return this;
 }

 use(mw: (ctx: APIContext) => any) {
   this.middlewares.add(mw);
 }

 get(uri: string, ...handlers: Array<(context?: any) => any>) {
   this.addRoute("GET", uri, ...handlers);
   return this;
 }
 post(uri: string, ...handlers: Array<(context?: any) => any>) {
   this.addRoute("POST", uri, ...handlers);
   return this;
 }
 put(uri: string, ...handlers: Array<(context?: any) => any>) {
   this.addRoute("PUT", uri, ...handlers);
   return this;
 }
 delete(uri: string, ...handlers: Array<(context?: any) => any>) {
   this.addRoute("DELETE", uri, ...handlers);
   return this;
 }

 private async handler(route: Route, context: APIContext): Promise<Response> {
   for (const middleware of this.middlewares) {
     const response = await middleware.call(this, context);
     if (response instanceof Response) {
       return response;
     }
   }

   // Run middlewares specific to this route
   for (const callback of route.callbacks) {
     const response = await callback.call(this, context);
     if (response instanceof Response) {
       return response;
     }
   }
   return route.callbacks.at(-1)?.call(this, context);
 }

 async init(context: APIContext): Promise<Response> {
   const path = context.url.pathname;
   console.log(path);
   const matchingRoute = this.routes.find((route) => {
     if (route.method.toLowerCase() !== context.request.method.toLowerCase()) {
       return false;
     }
     const match = path.match(route.regex);
     console.log({ [route.uri]: route });
     console.log(match);
     if (match) {
       // context.params = {};
       // for (let i = 0; i < route.keys.length; i++) {
       //   context.params[route.keys[i].name] = match[i + 1];
       // }
       return true;
     }
     return false;
   });
   console.log(matchingRoute);
   if (matchingRoute) {
     return await this.handler(matchingRoute, context);
   } else {
     return new Response("Not Found, 404", { status: 404 });
   }
 }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment