An elegant and fast URL router for service workers (and standalone use)
Yet another router? 😄
I was unable to find a modern router with the following features:
- Framework agnostic and service worker support
- Most routers are intertwined with a specific web server or framework, this one is agnostic and can be used everywhere (Node.js, browsers, workers). See the standalone example.
- The router is used in production with Cloudflare Workers.
- TypeScript (and JavaScript) support
- Even when not using TypeScript there's the benefit of better code editor tooling (improved IntelliSense) for the developer.
- Match the path or the full URL
- Most routers only support matching a
/path
, with service workers it's sometimes necessary to use the full URL as well.
- Most routers only support matching a
- Elegant pattern matching
- Life's too short to debug regexes. :-)
- Also: Lightweight (8KB, ~100 LOC), tested, supports tree shaking and ES modules
Installation
yarn add service-worker-router# or npm install --save service-worker-router
Usage
// TypeScript // Modern JavaScript, Babel, Webpack, Rollup, etc. // Legacy JavaScript and Node.js // Inside a web/service workerimportScripts'https://unpkg.com/service-worker-router' // HTML: Using ES modulesscript type="module" ;/script // HTML: Oldschoolscript src="https://unpkg.com/service-worker-router"/script
URL
polyfill
The router is making use of the WHATWG URL object. If your environment is Node < v8 or IE (see compat) you need to polyfill it before requiring/importing the router. By using polyfill.io the shim will only be loaded if the browser needs it.
// Add URL polyfill in Node.js < 8// npm i --save universal-urlrequire'universal-url'.shim // Add URL polyfill in workersimportScripts'https://cdn.polyfill.io/v2/polyfill.min.js?features=URL' // Add URL polyfill in HTML scriptsscript src="https://cdn.polyfill.io/v2/polyfill.min.js?features=URL"/script
Example (service worker)
JavaScript
// Instantiate a new routerconst router = // Define user handlerconst user = async { const headers = 'x-user-id': paramsid const response = `Hello user with id .` headers return response} // Define minimal ping handlerconst ping = async 'pong' // Define routes and their handlersrouterrouterall'/_ping' ping // Set up service worker event listener
TypeScript
Same as the above but with optional types:
// Add 'webworker' to the lib property in your tsconfig.json// also: https://github.com/Microsoft/TypeScript/issues/14877declare // Instantiate a new router // Define user handler // Define minimal ping handler // Define routes and their handlersrouter.get'/user/:id', userrouter.all'/_ping', ping // Set up service worker event listener// To resolve 'FetchEvent' add 'webworker' to the lib property in your tsconfig.jsonself.addEventListener'fetch',
Example (standalone)
This router can be used on it's own using router.match
, service worker usage is optional.
const router = const user = async `Hey there!`router router// => { params: { name: 'bob' }, handler: [AsyncFunction: user], url...
Patterns
The router is using the excellent url-pattern
module (it's sole dependency).
Patterns can have optional segments and wildcards.
A route pattern can be a string or a UrlPattern instance, for greater flexibility and optional regex support.
Examples
// will match everythingrouterall'*' handler // `id` value will be available in `params` in handlerrouterall'/api/users/:id' handler // will only match exact pathrouterall'/api/foo/' handler // will match longer paths as wellrouterall'/api/foo/*' handler // will match with wildcard in betweenrouterall'/admin/*/user/*/tail' handler // use UrlPattern instancerouterall'/api/posts(/:id)' handler
URL matching
By default the router will only match against the /path
of a URL. To test against a full URL just add { matchUrl: true }
when adding a route.
Examples
// test against full url, not only pathrouter // test against full url and extract segmentsrouter router// => { params: {subdomain: 'mail', domain: 'google', tld: 'com', _: 'mail'}, handler: [AsyncFunction], ...
Refer to the url-pattern
documentation and it's tests for more information and examples regarding pattern matching.
HTTP methods
To add a route, simply use one of the following methods. router.all
will match any HTTP method.
- router.all(pattern, handler, options)
- router.get(pattern, handler, options)
- router.post(pattern, handler, options)
- router.put(pattern, handler, options)
- router.patch(pattern, handler, options)
- router.delete(pattern, handler, options)
- router.head(pattern, handler, options)
- router.options(pattern, handler, options)
The function signature is as follows:
pattern: string | UrlPattern
handler: HandlerFunction
options: RouteOptions = {}
The RouteOptions
object is optional and can contain { matchUrl: boolean }
.
All methods will return the router instance, for optional chaining.
Handler function
The handler function for a route is expected to be an async
function (or Promise
).
// See the HandlerContext interface below for all available paramsconst handler = async { return 'Hello'}
When used in a service worker context the handler must return a Response object, if the route matches.
When used in conjunction with helper methods like router.handleRequest
and router.handleEvent
the handler function will be called automatically with an object, containing the following signature:
API
Match
url: URL | string, method: string
): MatchResult | null
router.match(Matches a supplied URL and HTTP method against the registered routes. url
can be a string (path or full URL) or URL instance.
router router// => { params: { id: '1337' }, handler: [AsyncFunction: handler], url...
The return value is a MatchResult
object or null
if no matching route was found.
request: Request
): MatchResult | null
router.matchRequest(Will match a Request object (e.g. event.request
) against the registered routes. Will return null
or a MatchResult
object.
event: FetchEvent
): MatchResult | null
router.matchEvent(Will match a FetchEvent object (e.g. event
) against the registered routes. Will return null
or a MatchResult
object.
Handle
url: URL | string, method: string
): HandleResult | null
router.handle(Will match a string or URL instance against the registered routes and call it's handler function automatically (with HandlerContext
).
const result = router
Will return null
or the matched route and handler promise as HandleResult
:
request: Request
): HandleResult | null
router.handleRequest(Will match a FetchEvent object against the registered routes and call it's handler function automatically (with HandlerContext
).
Will return null
or the matched route and handler promise as HandleResult
.
event: FetchEvent
): HandleResult | null
router.handleEvent(Will match a FetchEvent object against the registered routes. If a route matches it's handler will be called automatically and passed to event.respondWith(handler)
. If no route matches nothing happens. :-)
Will return null
or the matched route and handler promise as HandleResult
.
v1.7.5
)
Context (Since You can optionally add a context (router.ctx = { foobar: 123 }
) to the router, which will be passed on to the handlers as part of HandlerContext
. An example (also how to do this type safe) can be found in the test fixture.
Limitations
- No middleware support
- In service workers one needs to respond with a definitive Response object (when responding to a fetch event), so this paradigm doesn't really fit here.
Examples
See also
- workbox-router
- sw-toolbox
License
MIT