# The service worker fetch event and routing

> How the FetchEvent interface works, how to respond with event.respondWith(), and patterns for routing requests to different handlers.

**In one line:** Every network request made by a page in the service worker's scope fires a `fetch` event in the worker. `event.respondWith(promise)` lets the worker intercept the request and return any `Response` — from cache, network, or constructed inline.

## The FetchEvent interface

A `FetchEvent` carries:

- `event.request` — the `Request` object, including `url`, `method`, `headers`, `destination`, and `mode`.
- `event.respondWith(responsePromise)` — call this to take control of the response. If you don't call it, the request falls through to the network as normal.
- `event.waitUntil(promise)` — extend the worker's lifetime past the event, for side effects like cache warming.
- `event.clientId` — the identifier of the client (tab) that made the request.
- `event.resultingClientId` — the new client ID if a navigation request creates a new client.

## Basic interception

```js
self.addEventListener('fetch', (event) => {
  // Only intercept requests to our own origin
  if (!event.request.url.startsWith(self.location.origin)) return;

  event.respondWith(
    caches.match(event.request).then((cached) => cached ?? fetch(event.request))
  );
});
```

Returning without calling `respondWith()` passes the request through to the browser's normal network stack.

## Request properties for routing decisions

| Property | Values and meaning |
|---|---|
| `request.destination` | `'document'`, `'script'`, `'image'`, `'font'`, `'fetch'`, `''` (XHR/fetch with no type) |
| `request.mode` | `'navigate'` (page navigation), `'cors'`, `'no-cors'`, `'same-origin'` |
| `request.method` | `'GET'`, `'POST'`, `'PUT'`, etc. |
| `request.url` | Full URL string; use `new URL(event.request.url)` for pathname matching |

## Routing patterns

### By destination

```js
self.addEventListener('fetch', (event) => {
  const { destination } = event.request;

  if (destination === 'image') {
    event.respondWith(cacheFirst(event.request, 'images'));
  } else if (destination === 'document') {
    event.respondWith(networkFirst(event.request, 'pages'));
  }
  // other requests fall through
});
```

### By URL prefix

```js
self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);

  if (url.pathname.startsWith('/api/')) {
    event.respondWith(networkOnly(event.request));
  } else if (url.pathname.startsWith('/static/')) {
    event.respondWith(cacheFirst(event.request, 'static'));
  }
});
```

### Navigation requests (for SPA offline support)

For single-page apps, all `navigate` requests should return the cached app shell:

```js
self.addEventListener('fetch', (event) => {
  if (event.request.mode === 'navigate') {
    event.respondWith(
      caches.match('/index.html').then((cached) => cached ?? fetch(event.request))
    );
    return;
  }
  // handle other request types…
});
```

## Constructing a Response inline

`respondWith()` accepts any `Response`, including one you construct:

```js
event.respondWith(
  new Response('<h1>Offline</h1>', {
    headers: { 'Content-Type': 'text/html' },
  })
);
```

This is the basis for offline fallback pages. See [Offline fallback](/reference/service-worker/offline-fallback/).

## What `respondWith` does not cover

- **Non-interceptable requests.** Requests with `mode: 'no-cors'` from `<link rel="preload">` or third-party iframes may not fire a `fetch` event depending on context.
- **Opaque responses.** Cross-origin requests fetched with `no-cors` produce opaque responses (`status: 0`). You can cache and return them, but cannot read their body or status.
- **POST and mutation requests.** The fetch event fires for POST too, but caching mutation responses requires careful design (see Background Sync for deferring mutations offline).

## Using Workbox for routing

Workbox's `registerRoute` is a cleaner way to express routing rules than a long `if`/`else` chain in the `fetch` listener:

```js
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst } from 'workbox-strategies';

registerRoute(({ request }) => request.destination === 'image', new CacheFirst());
registerRoute(({ request }) => request.mode === 'navigate', new NetworkFirst());
```

See [Workbox](/reference/service-worker/workbox/) for full details.

## Practical checklist

- [ ] Only call `event.respondWith()` when you intend to handle the request; let everything else fall through.
- [ ] Check `event.request.method` before caching — never cache non-GET responses unless you specifically need to.
- [ ] Use `new URL(event.request.url).origin` to distinguish same-origin from cross-origin requests before routing.
- [ ] Handle the case where both cache and network fail — return a fallback `Response` to avoid a blank error page.
- [ ] Test routing logic with DevTools Network throttling set to "Offline".

## Cross-references

- [Caching strategies](/reference/service-worker/caching-strategies/) — the strategies invoked by `respondWith()`
- [The Cache API](/reference/service-worker/cache-api/) — `caches.match()` and related methods used inside handlers
- [Offline fallback](/reference/service-worker/offline-fallback/) — constructing a meaningful response when everything fails
- [Workbox](/reference/service-worker/workbox/) — production-ready routing and strategy library