Skip to content

Instantly share code, notes, and snippets.

@jasonbyrne
Created May 17, 2022 11:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jasonbyrne/7df6b0812fae9ad8b53469e7d11dca4f to your computer and use it in GitHub Desktop.
Save jasonbyrne/7df6b0812fae9ad8b53469e7d11dca4f to your computer and use it in GitHub Desktop.
CloudFlare Workers Router in TypeScript v2
export function helloWorld(): Response {
return new Response('Hello World', {
status: 200,
})
}
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
}
}
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)
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