➰ An elegant and fast URL router for service workers (and standalone use)
Although this router works fine I made a new one, based on experiences using it in production with Cloudflare Workers.
tiny-request-router
is even smaller, even less opinionated and more flexible to use. It also uses path-to-regexp
instead of url-pattern
, as I found it more intuitive to use. I’d recommend using the new router for new projects.
An elegant and fast URL router for service workers (and standalone use)
I was unable to find a modern router with the following features:
/path
, with service workers it’s sometimes necessary to use the full URL as well.yarn add service-worker-router
# or
npm install --save service-worker-router
// TypeScript
import { Router, HandlerContext } from 'service-worker-router'
// Modern JavaScript, Babel, Webpack, Rollup, etc.
import { Router } from 'service-worker-router'
// Legacy JavaScript and Node.js
const { Router } = require('service-worker-router')
// Inside a web/service worker
importScripts('https://unpkg.com/service-worker-router')
const Router = self.ServiceWorkerRouter.Router
// HTML: Using ES modules
<script type="module">
import { Router } from 'https://unpkg.com/service-worker-router/dist/router.browser.mjs';
</script>
// HTML: Oldschool
<script src="https://unpkg.com/service-worker-router"></script>
var Router = window.ServiceWorkerRouter.Router
URL
polyfillThe 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-url
require('universal-url').shim()
// Add URL polyfill in workers
importScripts('https://cdn.polyfill.io/v2/polyfill.min.js?features=URL')
// Add URL polyfill in HTML scripts
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=URL"></script>
// Instantiate a new router
const router = new Router()
// Define user handler
const user = async ({ request, params }) => {
const headers = new Headers({ 'x-user-id': params.id })
const response = new Response(`Hello user with id ${params.id}.`, { headers })
return response
}
// Define minimal ping handler
const ping = async () => new Response('pong')
// Define routes and their handlers
router.get('/user/:id', user)
router.all('/_ping', ping)
// Set up service worker event listener
addEventListener('fetch', event => {
// Will test event.request against the defined routes
// and use event.respondWith(handler) when a route matches
router.handleEvent(event)
})
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/14877
declare const self: ServiceWorkerGlobalScope
// Instantiate a new router
const router = new Router()
// Define user handler
const user = async ({ request, params }: HandlerContext): Promise<Response> => {
const headers = new Headers({ 'x-user-id': params.id })
const response = new Response(`Hello user with id ${params.id}.`, { headers })
return response
}
// Define minimal ping handler
const ping = async () => new Response('pong')
// Define routes and their handlers
router.get('/user/:id', user)
router.all('/_ping', ping)
// Set up service worker event listener
// To resolve 'FetchEvent' add 'webworker' to the lib property in your tsconfig.json
self.addEventListener('fetch', (event: FetchEvent) => {
// Will test event.request against the defined routes
// and use event.respondWith(handler) when a route matches
router.handleEvent(event)
})
This router can be used on it’s own using router.match
, service worker usage is optional.
const router = new Router()
const user = async () => `Hey there!`
router.get('/user/:name', user)
router.match('/user/bob', 'GET')
// => { params: { name: 'bob' }, handler: [AsyncFunction: user], url...
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.
// will match everything
router.all('*', handler)
// `id` value will be available in `params` in handler
router.all('/api/users/:id', handler)
// will only match exact path
router.all('/api/foo/', handler)
// will match longer paths as well
router.all('/api/foo/*', handler)
// will match with wildcard in between
router.all('/admin/*/user/*/tail', handler)
// use UrlPattern instance
router.all(new UrlPattern('/api/posts(/:id)'), handler)
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.
// test against full url, not only path
router.post('(http(s)\\://)api.example.com/users(/:id)', handler, {
matchUrl: true
})
// test against full url and extract segments
router.get('(http(s)\\://)(:subdomain.):domain.:tld(/*)', handler, {
matchUrl: true
})
router.match('http://mail.google.com/mail', 'GET')
// => { 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.
To add a route, simply use one of the following methods. router.all
will match any HTTP method.
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.
The handler function for a route is expected to be an async
function (or Promise
).
// See the HandlerContext interface below for all available params
const handler = async ({ request, params }) => {
return new Response('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:
interface HandlerContext {
params: any | null
handler: HandlerFunction
url: URL
method: string
route: Route
request?: Request
event?: FetchEvent
ctx: any
}
url: URL | string, method: string
): MatchResult | null
Matches a supplied URL and HTTP method against the registered routes. url
can be a string (path or full URL) or URL instance.
router.get('/user/:id', handler)
router.match('/user/1337', 'GET')
// => { params: { id: '1337' }, handler: [AsyncFunction: handler], url...
The return value is a MatchResult
object or null
if no matching route was found.
interface MatchResult {
params: any | null
handler: HandlerFunction
url: URL
method: string
route: Route
request?: Request
event?: FetchEvent
ctx: any
}
request: Request
): MatchResult | null
Will match a Request object (e.g. event.request
) against the registered routes. Will return null
or a MatchResult
object.
addEventListener('fetch', event => {
const match = router.matchRequest(event.request)
console.log(match)
// => { params: { user: 'bob' }, handler: [AsyncFunction: handler], ...
})
event: FetchEvent
): MatchResult | null
Will match a FetchEvent object (e.g. event
) against the registered routes. Will return null
or a MatchResult
object.
addEventListener('fetch', event => {
const match = router.matchEvent(event)
console.log(match)
// => { params: { user: 'bob' }, handler: [AsyncFunction: handler], ...
})
url: URL | string, method: string
): HandleResult | null
Will match a string or URL instance against the registered routes and call it’s handler function automatically (with HandlerContext
).
const result = router.handle('/user/bob', 'GET')
Will return null
or the matched route and handler promise as HandleResult
:
interface HandleResult {
match: MatchResult
handlerPromise: HandlerPromise
}
request: Request
): HandleResult | null
Will match a FetchEvent object against the registered routes and call it’s handler function automatically (with HandlerContext
).
addEventListener('fetch', event => {
const result = router.handleRequest(event.request)
if (result) {
event.respondWith(result.handlerPromise)
} else {
console.log('No route matched.')
}
})
Will return null
or the matched route and handler promise as HandleResult
.
event: FetchEvent
): HandleResult | null
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. 😃
addEventListener('fetch', event => {
router.handleEvent(event)
})
Will return null
or the matched route and handler promise as HandleResult
.
v1.7.5
)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.
MIT