Created
January 27, 2022 18:26
-
-
Save MichaelFedora/c4a0e751193c1b854f4175a516c96a5c to your computer and use it in GitHub Desktop.
Another tiny express-like http router thing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Server } from 'https://deno.land/[email protected]/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