# Wayne: an Express style router that runs inside a Service Worker

> Wayne is a JavaScript library that gives a Service Worker an Express like routing API, so a worker can answer requests with responses that never leave the browser, which makes it a mock server, a fixture filesystem and a request interceptor in one file. It ships as @jcubic/wayne on npm, has a file system middleware, and has moved fast, with three releases in the last month.

**jcubic/wayne** — Wayne: Open Source Service Worker Routing library for in browser HTTP requests

- Repository: https://github.com/jcubic/wayne
- Website: https://jcubic.github.io/wayne
- Stars: 585 · Forks: 31
- Language: JavaScript
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/jcubic-wayne

## A router whose responses never reach the network

Wayne is described as an open source Service Worker routing library for in browser HTTP requests, and the README puts it in one line: it's like an Express inside a Service Worker.

The premise is worth restating because it is unusual. Most of the time a Service Worker is used for caching HTTP requests and making the app work when there is no internet, mostly for progressive web apps, but the README points out that in fact you can create completely new responses to requests that never leave the browser. Wayne adds a familiar routing API on top of that ability.

So the worker stops being a cache and becomes a server. The keyword list in the package manifest tells you what people use it for: service-worker, server, http, http-server, fake, mock, artifical, download, hijack, router, filesystem, routing, http-interceptor, proxy, sse and server-sent-events. That is a wide spread, but it is one idea seen from different angles: a worker that can respond, redirect and serve files itself.

The routing surface is the familiar one. Routes are registered with methods and path patterns, and path parameters are read from req.params:

```javascript
const users = {
  1: 'Jakub T. Jankiewicz',
  2: 'John Doe',
  3: 'Jane Doe'
};

app.get('/user/{id}', function(req, res) {
  const user = users[req.params.id];
  if (user) {
    res.json({result: user});
  } else {
    res.json({error: 'User Not Found'});
  }
});
```

The response object carries json, text and redirect, and a status can be set alongside the body, so the responses you produce are ordinary HTTP responses rather than a special format.

## Three ways to load the library, depending on the worker type

The README documents three installation shapes, and the choice is forced by how the service worker itself is registered.

When the worker is an ES module, the library is imported from a CDN and instantiated:

```javascript
import { Wayne } from 'https://cdn.jsdelivr.net/npm/@jcubic/wayne';

const app = new Wayne();
```

When the worker is a classic script, the UMD build is loaded with importScripts and the constructor is namespaced:

```javascript
importScripts('https://cdn.jsdelivr.net/npm/@jcubic/wayne/index.umd.min.js');

const app = new wayne.Wayne();
```

And when a bundler such as Vite is in play, the import is the ordinary package import:

```javascript
import { Wayne } from '@jcubic/wayne';
```

The package manifest explains the build outputs this comes from. The type is module, the main entry is index.min.js, and the repository root holds index.js, index.min.js, index.umd.js and index.umd.min.js side by side, which is what makes all three loading styles possible from one package.

Installation from npm is the usual route:

```bash
npm install @jcubic/wayne
```

or with yarn:

```bash
yarn add @jcubic/wayne
```

The registration code that installs the worker is also documented in two variants, and the difference matters. The standard one registers with the type module option and computes the scope from the current path:

```javascript
if ('serviceWorker' in navigator) {
    const scope = location.pathname.replace(/\/[^\/]+$/, '/');
    navigator.serviceWorker.register('sw.js', { scope, type: 'module' })
```

The alternative exists for browsers that do not support ES modules in service workers, and it is the same call without the type module option, and therefore with the classic script loading path.

## Taking control on first load and skipping the waiting phase

Two service worker lifecycle details decide whether your routes work on the first page load, and the README covers both.

The first is control. According to the spec, the default behaviour of a service worker is to control the HTTP requests after reloading the page, which means your first visit is not intercepted. The fix is to claim the uncontrolled clients on activation:

```javascript
// take control of uncontrolled clients on first load
// ref: https://web.dev/service-worker-lifecycle/#clientsclaim
self.addEventListener('activate', (event) => {
  event.waitUntil(clients.claim());
});
```

The second is updates. Without intervention the new worker waits for every tab to be closed before it takes over, and in development that is a familiar frustration. The README offers the skipWaiting call on install:

```javascript
// don't wait for client when worker changes
self.addEventListener('install', (event) => {
   self.skipWaiting();
});
```

And there is a note for the current version: in 0.20.0 you can use `wayne.force();` to run both.

Together these three lines are the difference between a mock server that works the first time and one that appears broken until you reload twice. It is unglamorous, and it is the kind of thing that costs an afternoon when it is missing.

## A catch-all route and a file system middleware

Two features make Wayne more than a request matcher.

The first is a catch-all route that lets you intercept by extension and header, which is how you build a rule that applies to every request:

```javascript
app.get('*', function(req, res) {
  const url = new URL(req.url);
  const extension = path.extname(url.pathname);
  const accept = req.headers.get('Accept');
  if (extension === '.js' && accept.match(/text\/html/)) {
    res.text('// Sorry no source code for you');
  } else {
    res.fetch(req);
  }
});
```

The README explains the effect: the comment is shown for every request to JavaScript files from the browser when they are opened in a new tab, because the browser sends the Accept: text/html header, and res.fetch passes everything else through to the real network.

The second is the file system middleware, which is what turns the worker into a filesystem. It takes a path library, an fs implementation, a mime type resolver and a prefix, and it is designed to work with an in browser filesystem:

```javascript
import { Wayne, FileSystem } from 'https://cdn.jsdelivr.net/npm/@jcubic/wayne';
import FS from "https://cdn.skypack.dev/@isomorphic-git/lightning-fs";
import mime from "https://cdn.skypack.dev/mime";
import path from "https://cdn.skypack.dev/path-browserify";

const { promises: fs } = new FS("__wayne__");

const app = new Wayne();

app.use(FileSystem({ path, fs, mime, prefix: '__fs__' }));
```

With that in place, URLs carrying the __fs__ prefix are read from the in browser filesystem rather than the network. The dependencies are passed in rather than bundled, which is the right call for a library that has to run in a worker, and the README notes the same code works with a non module worker with minor changes.

## The build is a Makefile of sed commands

The repository is small and its build system is a good indicator of how the project is run. The Makefile holds a version, a date and a year, and the default target builds README.md, package-lock.json and the four JavaScript bundles.

Version bumping is done with sed rather than a script. The package.json target rewrites the version string in place, the README target rewrites the badge that carries the version and the year, and index.js gets the same treatment for the version comment in its header.

The bundles are then produced with browserify and uglifyjs. The UMD build runs npx browserify with the esmify plugin to turn the ES module into a UMD bundle, the minified UMD file is produced with npx uglifyjs, and the same minification is applied to index.js to produce index.min.js. The banner is a separate file, banner.js, with a version and date placeholder that is expanded into banner.version.js and prepended to each bundle.

So the release process is: bump the version in the Makefile, run make, and commit the regenerated files. The devDependencies match that story, with browserify, esmify, uglify-js and babelify plus babel core and preset env for the transpilation step.

The consequence for a consumer is that the repository ships the built artefacts, index.js, index.min.js, index.umd.js and index.umd.min.js, rather than asking you to run a build. That is convenient, and it is also why the version in the manifest, 0.20.2, and the version in the Makefile have to stay in step.

The release history shows how fast that happens. The tags are 0.20.2 and 0.20.1, both on 2026-08-03, and 0.20.0 on 2026-07-08, and the last push to the repository was on 2026-08-03, the same day as the newest two tags.

## An MIT library with a fast release cadence

The licence is MIT, with the file at the repository root, so there is nothing to negotiate if you vendor the library or ship it inside a product.

The maintenance picture is the interesting part. Three versions in the last month, two of them on the same day, and the last push to the repository on 2026-08-03. The repository is not archived, and the project is under the @jcubic scope on npm, authored by Jakub T. Jankiewicz, who also wrote the isomorphic git family of libraries that the file system middleware example uses.

The changelog is in the repository, and the README points at a wiki at jcubic.github.io/wayne for the rest of the documentation. The repository also carries a LICENSE, a Makefile, a .npmignore for the package contents, and a scripts and assets directory.

What Wayne is not is worth saying plainly. It is not a server, it runs only in the browser, and the entire value of the idea depends on the fact that requests can be answered without leaving the page. That is a genuinely useful capability in three situations: a development environment where you want the API mocked without a backend, an offline mode where fixtures are served from an in browser filesystem, and a test harness where the request layer is under your control. In all three, the Express style API is not decoration, it is the reason you can write the mock in the same shape as the real server code.

The risk of the fast cadence is the one you would expect from a 0.x library with three releases in a month. The API surface is still moving, so pinning a version is more than a formality, and the README already documents a helper, force(), that arrived in 0.20.0 to fold two lifecycle workarounds into one call.

## Conclusion

Adopt Wayne if you build browser applications and want a mock server or a fixture filesystem that lives in the Service Worker, so the same code can answer requests offline, in tests, or behind a real API without changing the client. Do not adopt it as a production server, because everything it does is browser scoped and the README is explicit that its purpose is in-browser HTTP routing. Verify first that your service worker registration shape matches one of the documented ones, module or classic script, since the library is loaded differently in each case and the ESM registration needs the type module option, and that the version you pin exposes the methods you need, because the API is moving quickly and the README notes a force helper added in 0.20.0.

## FAQ

### What is Wayne?

Wayne is an open source Service Worker routing library for in browser HTTP requests, described as being like an Express inside a Service Worker. It lets a service worker create new responses to requests that never leave the browser, using a routing API with methods, path parameters, JSON, text and redirect responses.

### How do I install Wayne?

Install the package with npm install @jcubic/wayne or yarn add @jcubic/wayne. The library is then loaded in the worker itself, from a CDN with an ES module import, from the UMD build with importScripts, or from the package name when a bundler such as Vite is used.

### Why do my Wayne routes not work on the first page load?

By default a service worker only controls requests after the page is reloaded. The README shows adding an activate handler that calls clients.claim() to take control of uncontrolled clients on first load, and an install handler that calls self.skipWaiting() so a changed worker does not wait for all tabs to close.

### Can a Wayne service worker serve files from the filesystem?

Yes, through the FileSystem middleware, which takes a path library, an fs implementation, a mime resolver and a prefix. The README's example combines it with the isomorphic git lightning-fs filesystem, and URLs with the __fs__ prefix are then read from that filesystem.

## Sources

- [jcubic/wayne on GitHub](https://github.com/jcubic/wayne)
- [License: MIT](https://github.com/jcubic/wayne/blob/master/LICENSE)
- [Project website](https://jcubic.github.io/wayne)
- [README](https://github.com/jcubic/wayne/blob/master/README.md)
- [Releases](https://github.com/jcubic/wayne/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/jcubic-wayne
