Service Worker
To get Toapi’s offline / tag-based revalidation behavior, add a service worker
built by vite-plugin-pwa in
injectManifest mode. The two plugins compose cleanly: @toapi/vite-plugin
redirects the client build to dist/client/, which is exactly where VitePWA
emits sw.js, and the production server bundle is built separately so nothing
leaks across.
Inside the worker the two responsibilities also compose cleanly:
- VitePWA / Workbox precaches your static build output (the app shell,
JS, CSS, images) via the manifest it injects as
self.__WB_MANIFEST. setupToapiWorkerhandles your Toapi API routes — caching, offline fallback, and tag-based revalidation.
Because setupToapiWorker’s fetch listener only responds to same-origin
requests under its basePath, everything else — every static asset — falls
through to Workbox’s precache route. The two never fight over a request.
Installation
Section titled “Installation”pnpm add -D vite-plugin-pwa workbox-precachingworkbox-precaching provides precacheAndRoute, which turns the injected
manifest into a static-asset cache. It ships with Workbox but is a separate
import, so install it explicitly (pnpm does not hoist transitive deps).
Vite config
Section titled “Vite config”import { defineConfig } from "vite";import toapi from "@toapi/vite-plugin";import { VitePWA } from "vite-plugin-pwa";
export default defineConfig({ plugins: [ toapi(), VitePWA({ strategies: "injectManifest", srcDir: "src", filename: "service-worker.ts", injectRegister: "auto", devOptions: { enabled: true, type: "module" }, // optional, pass-through to VitePWA: // manifest: { name: "My App", short_name: "App", ... }, }), ],});Setting devOptions.enabled: true makes the service worker run during
vite dev as well. Without it the SW only runs in vite preview and
production.
Service worker
Section titled “Service worker”Precache the static build output with Workbox, then hand your API routes to Toapi — two calls, one file:
import { precacheAndRoute } from "workbox-precaching";import { setupToapiWorker } from "@toapi/worker";
declare const self: ServiceWorkerGlobalScope & { // VitePWA injects the precache manifest here in injectManifest mode. __WB_MANIFEST: Array<{ url: string; revision: string | null }>;};
// 1. Cache static build files (app shell, JS, CSS, images) via VitePWA/Workbox.precacheAndRoute(self.__WB_MANIFEST);
// 2. Cache & revalidate Toapi API routes.setupToapiWorker();precacheAndRoute(self.__WB_MANIFEST) registers a fetch route for every
static file VitePWA fingerprinted at build time, so navigations and assets work
offline. setupToapiWorker() adds its own fetch listener that only claims
same-origin requests under /api (skipping /api/__tapi), so the two coexist:
static requests are served from the precache, API requests from the Toapi cache.
If your API lives somewhere other than /api, pass a matching basePath — and
make sure it isn’t a prefix VitePWA also precaches:
setupToapiWorker({ basePath: "/data" });TypeScript
Section titled “TypeScript”Add "WebWorker" to the lib array in tsconfig.json so TypeScript
recognizes ServiceWorkerGlobalScope and related globals:
{ "compilerOptions": { "lib": ["ESNext", "DOM", "WebWorker"] }}- Match
setupToapiWorker’sbasePathto thebasePathyou pass totoapi(). It defaults to/api. - Pass
maximumStaleAgetosetupToapiWorkerto tune the grace period, in seconds, past a cache entry’sexpiresAtbefore it is deleted by the cleanup pass that runs on every worker startup. It defaults to 7 days. - Need to interleave your own
fetchlogic with Toapi’s? Callcleanup,handleToapiRequest, andlistenForInvalidationsdirectly instead ofsetupToapiWorker.
For the full service-worker API, see the
setupToapiWorker reference and the
@toapi/worker package.