Skip to content

Instantly share code, notes, and snippets.

@MichaelFedora
Created January 27, 2022 18:26
Show Gist options
  • Save MichaelFedora/c4a0e751193c1b854f4175a516c96a5c to your computer and use it in GitHub Desktop.
Save MichaelFedora/c4a0e751193c1b854f4175a516c96a5c to your computer and use it in GitHub Desktop.
Another tiny express-like http router thing
export interface TinyRequest {
// CORE (std)
url: string;
method: string;
headers: Headers;
json<T = unknown>(): Promise<T>;
text(): Promise<string>;
// BONUS (tiny)
params?: Record<string, unknown>;
query?: Record<string, unknown>;
session?: string;
// deno-lint-ignore no-explicit-any
user?: any;
}
export type RouteHandler = (req: TinyRequest, next: () => Promise<Response> | Response) => Promise<Response> | Response;
export interface Route {
route: string;
type: 'GET' | 'POST' | 'PUT' | 'DELETE';
handler: RouteHandler;
}
import { TinyRequest, RouteHandler } from './api-types.ts';
interface RouteStep {
route: string;
type?: 'GET' | 'POST' | 'PUT' | 'DELETE';
handler: Router | RouteHandler;
}
export class Router {
readonly #steps: RouteStep[] = [];
constructor() { }
#condense(handlers: readonly RouteHandler[]): RouteHandler {
if(!handlers?.length)
return (_, next) => next();
if(handlers.length === 1)
return handlers[0];
return async (ctx, next) => {
let i = 0;
const call = () => handlers[i] ? handlers[i++](ctx, call) : next();
return await call();
};
}
use(route: string, handler: Router | RouteHandler, ...handlers: (Router | RouteHandler)[]): this;
use(handler: Router | RouteHandler, ...handlers: (Router | RouteHandler)[]): this;
use(routeOrHandler: string | Router | RouteHandler, ...handlers: (Router | RouteHandler)[]): this {
const route = (!routeOrHandler || typeof routeOrHandler === 'string' ? routeOrHandler : '') || '/';
if(typeof routeOrHandler === 'function' || routeOrHandler instanceof Router)
handlers.unshift(routeOrHandler);
for(const handler of handlers) {
this.#steps.push({
route,
handler
});
}
return this;
}
#add(route: string, type: 'GET' | 'POST' | 'PUT' | 'DELETE', handlers: RouteHandler[]): void {
// enforce `/{route}` with no trailing `/`'s
route = '/' + route.replace(/^\/+|\/+$/g, '');
this.#steps.push({
route,
type,
handler: this.#condense(handlers)
});
}
get(route: string, handler: RouteHandler, ...handlers: RouteHandler[]): this {
handlers.unshift(handler);
this.#add(route, 'GET', handlers);
return this;
}
post(route: string, handler: RouteHandler, ...handlers: RouteHandler[]): this {
handlers.unshift(handler);
this.#add(route, 'POST', handlers);
return this;
}
put(route: string, handler: RouteHandler, ...handlers: RouteHandler[]): this {
handlers.unshift(handler);
this.#add(route, 'PUT', handlers);
return this;
}
delete(route: string, handler: RouteHandler, ...handlers: RouteHandler[]): this {
handlers.unshift(handler);
this.#add(route, 'DELETE', handlers);
return this;
}
#matchRoute(url: string, route: string, type?: string): boolean {
let pattern = new URLPattern({ pathname: route });
let test = pattern.test(url);
if(type || test)
return test;
pattern = new URLPattern({ pathname: route + '(.*)' });
test = pattern.test(url);
return test;
}
#pathfind(req: TinyRequest, base = ''): RouteHandler[] {
// enforce `/{route}` with no trailing `/`'s
if(base)
base = '/' + base.replace(/^\/+|\/+$/g, '');
const path: RouteHandler[] = [];
for(const step of this.#steps) {
const route = (base + step.route).replace(/\/+$/g, '');
if( (step.type && req.method !== step.type) ||
!this.#matchRoute(req.url, route, step.type) )
continue;
if(step.handler instanceof Router)
path.push(...step.handler.#pathfind(req, route));
else {
path.push((req, next) => {
req.params = (new URLPattern({ pathname: route })).exec(req.url)?.pathname?.groups;
req.query = req.url.includes('?')
? Object.fromEntries((new URLSearchParams(req.url.slice(req.url.indexOf('?')))).entries())
: undefined;
return (step.handler as RouteHandler)(req, next);
});
}
}
return path;
}
async process(req: TinyRequest, base = ''): Promise<Response | undefined> {
const path = this.#pathfind(req, base);
const nextResponse = new Response();
const res = await this.#condense(path)(req, () => nextResponse);
if(!res || res === nextResponse)
return undefined;
return res;
}
}
export default Router;
import { Server } from 'https://deno.land/std@0.122.0/http/server.ts'
import { handleError } from './middleware.ts';
import { text, json } from './api-util.ts';
import Router from './router.ts';
const router = new Router();
router.get('/test/:neat', req => json({ params: req.params, query: req.query }));
const rootHandleError = handleError('root');
const app = new Server({
handler: req => rootHandleError(req, async () => {
const res = await router.process(req);
if(res)
return res;
return text('Not Found', { status: 404 });
}),
port: 3000
});
app.listenAndServe();
console.log('Serving on "http://localhost:3000/"!');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment