A Fastify-like router for Service Workers that enables handling HTTP requests directly in service workers with a familiar, ergonomic API.
β οΈ Early Stage Project: This project is in very early development (started September 20, 2025). APIs may change, and some features might not be fully stable yet. Use with caution in production environments.
I'm a fan of this frameworkβs simplicity, and I really wanted an easy way to build CSR SPAs (Client-Side Rendered Single-Page Applications).
Give it a try β I'm sure you'll love it. Workerify works seamlessly with htmx.
There are certainly plenty of other use cases (I have a few in mind π), but I'd love to hear your ideas. Come share them with us on Discord.
- π Fastify-like API - Familiar routing patterns with
GET,POST,PUT,DELETE, etc. - π§ Vite Integration - First-class Vite plugin for seamless development
- π‘ BroadcastChannel Communication - Efficient message passing between main thread (or web worker) and service worker
- π― Type-Safe - Full TypeScript support with comprehensive type definitions
- β‘ Zero Dependencies - Lightweight core library with no runtime dependencies
- π Hot Route Replacement - Development mode with automatic route updates
- π Multi-Tab Support - Intelligent client isolation prevents interference between browser tabs
The example made with Workerify and htmx is available here
npx @workerify/create-htmx-app# Using pnpm
pnpm add @workerify/lib @workerify/vite-plugin
# Using npm
npm install @workerify/lib @workerify/vite-plugin
# Using yarn
yarn add @workerify/lib @workerify/vite-pluginAdd the Workerify plugin to your vite.config.ts:
import { defineConfig } from 'vite';
import workerify from '@workerify/vite-plugin';
export default defineConfig({
plugins: [workerify()]
});Or
import { defineConfig } from 'vite';
import workerify from '@workerify/vite-plugin';
export default defineConfig({
plugins: [
workerify({
scope: '/', // Service worker scope
swFileName: 'sw.js' // Service worker file name
})
]
});import { Workerify } from '@workerify/lib';
const app = new Workerify({ logger: true });
// Simple GET route
app.get('/api/hello', async (request, reply) => {
return { message: 'Hello from Service Worker!' };
});
// Route with parameters
app.get('/api/users/:id', async (request, reply) => {
const userId = request.params.id;
return { userId, name: `User ${userId}` };
});
// POST route with body handling
app.post('/api/users', async (request, reply) => {
const data = request.body;
reply.status = 201;
return { id: Date.now(), ...data };
});
// Prefix matching for catch-all routes (using wildcard syntax)
app.get('/api/*', async (request, reply) => {
return { message: 'Catch-all API route', path: request.url };
});
// Start listening for requests (now async)
await app.listen();In your main application:
import { registerWorkerifySW } from 'virtual:workerify-register';
import { Workerify } from '@workerify/lib';
// Register the service worker
await registerWorkerifySW();
// Create and use your Workerify instance
const app = new Workerify({ logger: true });
// ... add your routes
await app.listen();TypeScript Support: The virtual module types are automatically included when you install
@workerify/vite-plugin. If TypeScript doesn't recognize them, add this to yourvite-env.d.ts:/// <reference types="@workerify/vite-plugin/client" />
In your browser's console:
const bc = new BroadcastChannel("workerify");
bc.postMessage({type:'workerify:routes:list'});Workerify automatically handles multiple browser tabs of the same application running simultaneously. Each tab operates in complete isolation:
- Consumer ID System: Each Workerify instance generates a unique consumer ID
- Client Mapping: Service worker maintains a map of client IDs to consumer IDs
- Request Routing: HTTP requests are routed to the correct tab based on the originating client
- Automatic Cleanup: Closed tabs are automatically cleaned up to prevent memory leaks
Check which tabs are registered and their consumer mappings:
const bc = new BroadcastChannel("workerify");
bc.postMessage({type:'workerify:clients:list'});
// Check console for detailed client informationThis ensures that opening multiple tabs of your application works seamlessly without interference between tabs.
const app = new Workerify(options?: WorkerifyOptions);Options:
logger?: boolean- Enable console logging (default:false)scope?: string- Service worker scope
// HTTP method routes (supports wildcard with /* suffix)
app.get(path: string, handler: RouteHandler);
app.post(path: string, handler: RouteHandler);
app.put(path: string, handler: RouteHandler);
app.delete(path: string, handler: RouteHandler);
app.patch(path: string, handler: RouteHandler);
app.head(path: string, handler: RouteHandler);
app.option(path: string, handler: RouteHandler); // Note: singular 'option'
// Route for all HTTP methods (supports wildcard with /* suffix)
app.all(path: string, handler: RouteHandler);
// Examples with wildcard:
app.get('/api/*', handler); // Matches /api/users, /api/posts, etc.
app.post('/admin/*', handler); // Matches all POST requests under /admin/
// Generic route registration with explicit matching control
app.route({
method?: HttpMethod, // Optional: specific HTTP method
path: string, // Route path
handler: RouteHandler, // Request handler function
match?: 'exact' | 'prefix' // Matching strategy (default: 'exact')
});
// Plugin registration
app.register(plugin: WorkerifyPlugin, options?: any): Promise<Workerify>;interface WorkerifyRequest {
url: string;
method: HttpMethod;
headers: Record<string, string>;
body?: ArrayBuffer | null;
params: Record<string, string>; // Route parameters
}interface WorkerifyReply {
status?: number;
statusText?: string;
headers?: Record<string, string>;
body?: any;
bodyType?: 'json' | 'text' | 'arrayBuffer';
}type RouteHandler = (
request: WorkerifyRequest,
reply: WorkerifyReply
) => Promise<any> | any;// Simple wildcard syntax - automatically enables prefix matching
app.get('/static/*', async (request, reply) => {
const url = new URL(request.url);
const path = url.pathname.replace('/static/', '');
const response = await fetch(`/assets/${path}`);
reply.status = response.status;
reply.headers = Object.fromEntries(response.headers);
reply.bodyType = 'arrayBuffer';
return await response.arrayBuffer();
});// Proxy all requests under /api/ to an external service
app.all('/api/*', async (request, reply) => {
const url = new URL(request.url);
const targetUrl = url.pathname.replace('/api/', 'https://api.example.com/');
const response = await fetch(targetUrl, {
method: request.method,
headers: request.headers,
body: request.body
});
reply.status = response.status;
reply.bodyType = 'json';
return await response.json();
});
// Alternative: Use route() for explicit control
app.route({
path: '/proxy/',
match: 'prefix', // Explicitly set prefix matching
handler: async (request, reply) => {
// Handle proxy logic
}
});const authenticate = async (request: WorkerifyRequest) => {
const token = request.headers['authorization'];
if (!token) throw new Error('Unauthorized');
// Validate token...
return { userId: '123' };
};
app.get('/api/protected', async (request, reply) => {
const user = await authenticate(request);
return { message: `Hello ${user.userId}` };
});This is a monorepo managed with pnpm workspaces. To contribute:
# Clone the repository
git clone https://github.com/yourusername/workerify.git
cd workerify
# Install dependencies
pnpm install
# Build all packages
pnpm build
# Run development mode
pnpm dev
# Run linting
pnpm lint
# Run type checking
pnpm typecheckworkerify/
βββ packages/
β βββ lib/ # Core Workerify library
β βββ vite-plugin/ # Vite plugin
β βββ examples/ # Example applications
β βββ htmx/ # HTMX example app
βββ package.json # Root package.json
βββ pnpm-workspace.yaml
βββ turbo.json # Turbo configuration
Workerify requires browsers that support:
- Service Workers
- BroadcastChannel API
- ES Modules
This includes all modern browsers (Chrome 66+, Firefox 57+, Safari 11.1+, Edge 79+).
MIT
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Inspired by Fastify's excellent API design and developer experience.