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

// a test file outside pages directory

import { Router } from "./router";
import type { APIContext, APIRoute } from "astro";

const router = new Router();

router.use((ctx) => {
console.log("Middleware");
});

router.get(
"/ping",
() => {
console.log("Route Middleware");
},
({ url }: APIContext) => {
return new Response("!Pong", { status: 200 });
}
);

router.get("/nice", () => {
return new Response("Yes", { status: 200 });
});

export const testRouter = router;

@KartikBazzad
Copy link
Author

UPDATE added trie router

import type { APIContext } from "astro";

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

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

export class Router {
  root: TrieNode = new TrieNode();
  routes: Array<Route> = [];
  middlewares: Set<(context: APIContext) => any> = new Set();
  constructor() {
    this.root = new TrieNode();
    this.routes = [];
    this.middlewares = new Set();
    return this;
  }

  private insertTrieNode(pathSegments: string[], route: Route) {
    let currentNode = this.root;
    for (const segment of pathSegments) {
      if (!currentNode.children[segment]) {
        currentNode.children[segment] = new TrieNode();
      }
      currentNode = currentNode.children[segment];
    }
    console.log("before", { root: this.root });
    currentNode.route = route;
    this.root = currentNode;
    console.log("After", { root: this.root });
  }
  private findMatchingRoute(path: string): Route | null {
    const pathSegments = path.split("/").filter(Boolean);
    console.log(pathSegments);
    let currentNode = this.root;
    for (const segment of pathSegments) {
      if (currentNode.children[segment]) {
        currentNode = currentNode.children[segment];
      }
    }
    return currentNode.route;
  }

  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`);
    }
    const route = { method, uri, callbacks: handlers };
    // this.routes.push();
    const pathSegments = route.uri.split("/").filter(Boolean);
    this.insertTrieNode(pathSegments, 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();

    const route = this.findMatchingRoute(path);
    // const route = this.routes.find((_route) => {
    //   return path.includes(_route.uri);
    // });
    console.log({ route });
    if (route) {
      if (route.method.toLowerCase() === context.request.method.toLowerCase()) {
        return await this.handler(route, context);
      } else {
        console.log("Hello");
        return new Response("Not Found, 404", { status: 200 });
      }
    }
    console.log("Not Found");
    return new Response("Not Found, 404", { status: 200 });
  }
}

@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