Created
May 17, 2022 11:32
-
-
Save jasonbyrne/7df6b0812fae9ad8b53469e7d11dca4f to your computer and use it in GitHub Desktop.
CloudFlare Workers Router in TypeScript v2
This file contains 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 function helloWorld(): Response { | |
return new Response('Hello World', { | |
status: 200, | |
}) | |
} |
This file contains 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
type QueryStringKeyValue = { [key: string]: string | true } | |
export class IncomingRequest { | |
public readonly url: URL | |
private qs: QueryStringKeyValue | |
private pathParts: string[] | |
public get request(): Request { | |
return this.event.request | |
} | |
public get fileName(): string { | |
return this.pathParts[this.pathParts.length - 1] | |
} | |
public get headers(): Headers { | |
return this.request.headers | |
} | |
public get path(): string { | |
return this.url.pathname | |
} | |
constructor(public readonly event: FetchEvent) { | |
this.url = new URL(event.request.url) | |
this.qs = Object.fromEntries(new URLSearchParams(this.url.search)) | |
this.pathParts = this.url.pathname.split('/') | |
} | |
public getPath(start?: number, length?: number): string { | |
return this.pathParts | |
.slice( | |
start || 0, | |
length !== undefined && length > 0 ? (start || 0) + length : undefined, | |
) | |
.join('/') | |
} | |
public queryString(key: string): string | true | null | |
public queryString<T>(key: string, filter: (val: string) => T): T | null | |
public queryString( | |
key: string, | |
filter?: (val: string) => string | number | boolean, | |
): string | number | boolean | null { | |
const val = this.qs[key] === undefined ? null : this.qs[key] | |
if (filter !== undefined && val !== null) { | |
return filter(String(val)) | |
} | |
return val | |
} | |
} |
This file contains 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 { helloWorld } from './hello-world' | |
import { Router } from './router' | |
const r = new Router() | |
r.all(helloWorld) | |
async function handleRequest(event: FetchEvent) { | |
r.execute(event) | |
} | |
addEventListener('fetch', handleRequest) |
This file contains 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 { IncomingRequest } from './incoming-request' | |
/** | |
* Helper functions that when passed a request will return a | |
* boolean indicating if the request uses that HTTP method, | |
* header, host or referrer. | |
*/ | |
const Method = (method: string) => (req: Request) => | |
req.method.toLowerCase() === method.toLowerCase() | |
const Connect = Method('connect') | |
const Delete = Method('delete') | |
const Get = Method('get') | |
const Head = Method('head') | |
const Options = Method('options') | |
const Patch = Method('patch') | |
const Post = Method('post') | |
const Put = Method('put') | |
const Trace = Method('trace') | |
const Header = (header: string, val: string | RegExp) => (req: Request) => { | |
if (typeof val == 'string') { | |
return req.headers.get(header) === val | |
} | |
return val.test(req.headers.get(header) || '') | |
} | |
const Host = (host: string | RegExp) => | |
Header('host', typeof host == 'string' ? host.toLowerCase() : host) | |
const Referrer = (referrer: string | RegExp) => | |
Header( | |
'referrer', | |
typeof referrer == 'string' ? referrer.toLowerCase() : referrer, | |
) | |
const Path = | |
(path: string | RegExp) => | |
(req: Request): boolean => { | |
const urlPath = new URL(req.url).pathname | |
return typeof path == 'string' ? path === urlPath : path.test(urlPath) | |
} | |
type Condition = (req: Request) => boolean | |
type Handler = (req: IncomingRequest) => Response | Promise<Response> | |
interface Route { | |
conditions: Condition[] | |
handler: Handler | |
} | |
/** | |
* The Router handles determines which handler is matched given the | |
* conditions present for each request. | |
*/ | |
export class Router { | |
public routes: Route[] = [] | |
handle(conditions: Condition[], handler: Handler): Router { | |
this.routes.push({ | |
conditions, | |
handler, | |
}) | |
return this | |
} | |
connect(pattern: string | RegExp, handler: Handler): Router { | |
return this.handle([Connect, Path(pattern)], handler) | |
} | |
delete(pattern: string | RegExp, handler: Handler): Router { | |
return this.handle([Delete, Path(pattern)], handler) | |
} | |
get(pattern: string | RegExp, handler: Handler): Router { | |
return this.handle([Get, Path(pattern)], handler) | |
} | |
head(pattern: string | RegExp, handler: Handler): Router { | |
return this.handle([Head, Path(pattern)], handler) | |
} | |
options(pattern: string | RegExp, handler: Handler): Router { | |
return this.handle([Options, Path(pattern)], handler) | |
} | |
patch(pattern: string | RegExp, handler: Handler): Router { | |
return this.handle([Patch, Path(pattern)], handler) | |
} | |
post(pattern: string | RegExp, handler: Handler): Router { | |
return this.handle([Post, Path(pattern)], handler) | |
} | |
put(pattern: string | RegExp, handler: Handler): Router { | |
return this.handle([Put, Path(pattern)], handler) | |
} | |
trace(pattern: string | RegExp, handler: Handler): Router { | |
return this.handle([Trace, Path(pattern)], handler) | |
} | |
any(pattern: string | RegExp, handler: Handler): Router { | |
return this.handle([Path(pattern)], handler) | |
} | |
host(hostName: string | RegExp, handler: Handler): Router { | |
return this.handle([Host(hostName)], handler) | |
} | |
referrer(referrer: string | RegExp, handler: Handler): Router { | |
return this.handle([Referrer(referrer)], handler) | |
} | |
all(handler: Handler): Router { | |
return this.handle([], handler) | |
} | |
route(e: FetchEvent): Response | Promise<Response> { | |
// Find a route that matches | |
const route = this.resolve(e.request) | |
// If we found one | |
if (route !== undefined) { | |
return route.handler(new IncomingRequest(e)) | |
} | |
// If not, fall back to a 404 | |
return new Response('resource not found', { | |
status: 404, | |
statusText: 'not found', | |
headers: { | |
'content-type': 'text/plain', | |
}, | |
}) | |
} | |
/** | |
* resolve returns the matching route for a request that returns | |
* true for all conditions (if any). | |
*/ | |
resolve(req: Request): Route | undefined { | |
return this.routes.find((r) => { | |
// If there are no conditions, this one automatically matches | |
if ( | |
!r.conditions || | |
(Array.isArray(r.conditions) && !r.conditions.length) | |
) { | |
return true | |
} | |
// Otherwise, they all have to match | |
return r.conditions.every((c) => c(req)) | |
}) | |
} | |
execute(event: FetchEvent): void { | |
return event.respondWith(this.route(event)) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment