Skip to content

Repository files navigation

Workerify

npm lib versionnpm vite-plugin versionLicense: MITTypeScriptpnpmDiscordPRs Welcome

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.

Why? For Htmx

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.

Features

  • 🚀 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

Demo

The example made with Workerify and htmx is available here

Quick start

npx @workerify/create-htmx-app

Manual Installation

1. Installation

# 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-plugin

2. Configure Vite

Add the Workerify plugin to your vite.config.ts:

import{defineConfig}from'vite';importworkerifyfrom'@workerify/vite-plugin';exportdefaultdefineConfig({plugins: [workerify()]});

Or

import{defineConfig}from'vite';importworkerifyfrom'@workerify/vite-plugin';exportdefaultdefineConfig({plugins: [workerify({scope: '/',// Service worker scopeswFileName: 'sw.js'// Service worker file name})]});

3. Create Your Worker Routes

import{Workerify}from'@workerify/lib';constapp=newWorkerify({logger: true});// Simple GET routeapp.get('/api/hello',async(request,reply)=>{return{message: 'Hello from Service Worker!'};});// Route with parametersapp.get('/api/users/:id',async(request,reply)=>{constuserId=request.params.id;return{ userId,name: `User ${userId}`};});// POST route with body handlingapp.post('/api/users',async(request,reply)=>{constdata=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)awaitapp.listen();

4. Register the Service Worker

In your main application:

import{registerWorkerifySW}from'virtual:workerify-register';import{Workerify}from'@workerify/lib';// Register the service workerawaitregisterWorkerifySW();// Create and use your Workerify instanceconstapp=newWorkerify({logger: true});// ... add your routesawaitapp.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 your vite-env.d.ts:

/// <reference types="@workerify/vite-plugin/client" />

5. List registered routes (Debug)

In your browser's console:

constbc=newBroadcastChannel("workerify");bc.postMessage({type:'workerify:routes:list'});

Multi-Tab Support

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

Debug Multi-Tab Setup

Check which tabs are registered and their consumer mappings:

constbc=newBroadcastChannel("workerify");bc.postMessage({type:'workerify:clients:list'});// Check console for detailed client information

This ensures that opening multiple tabs of your application works seamlessly without interference between tabs.

API Reference

Workerify Class

constapp=newWorkerify(options?: WorkerifyOptions);

Options:

  • logger?: boolean - Enable console logging (default: false)
  • scope?: string - Service worker scope

Route Methods

// 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 controlapp.route({method?: HttpMethod,// Optional: specific HTTP methodpath: string,// Route pathhandler: RouteHandler,// Request handler functionmatch?: 'exact'|'prefix'// Matching strategy (default: 'exact')});// Plugin registrationapp.register(plugin: WorkerifyPlugin,options?: any): Promise<Workerify>;

Request Object

interfaceWorkerifyRequest{url: string;method: HttpMethod;headers: Record<string,string>;body?: ArrayBuffer|null;params: Record<string,string>;// Route parameters}

Reply Object

interfaceWorkerifyReply{status?: number;statusText?: string;headers?: Record<string,string>;body?: any;bodyType?: 'json'|'text'|'arrayBuffer';}

Route Handler

typeRouteHandler=(request: WorkerifyRequest,reply: WorkerifyReply)=>Promise<any>|any;

Examples

Static File Serving

// Simple wildcard syntax - automatically enables prefix matchingapp.get('/static/*',async(request,reply)=>{consturl=newURL(request.url);constpath=url.pathname.replace('/static/','');constresponse=awaitfetch(`/assets/${path}`);reply.status=response.status;reply.headers=Object.fromEntries(response.headers);reply.bodyType='arrayBuffer';returnawaitresponse.arrayBuffer();});

API Proxy

// Proxy all requests under /api/ to an external serviceapp.all('/api/*',async(request,reply)=>{consturl=newURL(request.url);consttargetUrl=url.pathname.replace('/api/','https://api.example.com/');constresponse=awaitfetch(targetUrl,{method: request.method,headers: request.headers,body: request.body});reply.status=response.status;reply.bodyType='json';returnawaitresponse.json();});// Alternative: Use route() for explicit controlapp.route({path: '/proxy/',match: 'prefix',// Explicitly set prefix matchinghandler: async(request,reply)=>{// Handle proxy logic}});

Middleware Pattern

constauthenticate=async(request: WorkerifyRequest)=>{consttoken=request.headers['authorization'];if(!token)thrownewError('Unauthorized');// Validate token...return{userId: '123'};};app.get('/api/protected',async(request,reply)=>{constuser=awaitauthenticate(request);return{message: `Hello ${user.userId}`};});

Development

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 typecheck

Project Structure

workerify/
├── 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

Browser Support

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+).

License

MIT

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Acknowledgments

Inspired by Fastify's excellent API design and developer experience.

About

Fastify-like server in your Service Worker (👋 Htmx)

Topics

Resources

Stars

11 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages