@depup/h3 2.0.1-depup.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -0
  3. package/bin/h3.mjs +36 -0
  4. package/changes.json +5 -0
  5. package/dist/THIRD-PARTY-LICENSES.md +70 -0
  6. package/dist/_entries/bun.d.mts +6 -0
  7. package/dist/_entries/bun.mjs +16 -0
  8. package/dist/_entries/cloudflare.d.mts +6 -0
  9. package/dist/_entries/cloudflare.mjs +16 -0
  10. package/dist/_entries/deno.d.mts +6 -0
  11. package/dist/_entries/deno.mjs +16 -0
  12. package/dist/_entries/generic.d.mts +6 -0
  13. package/dist/_entries/generic.mjs +16 -0
  14. package/dist/_entries/node.d.mts +10 -0
  15. package/dist/_entries/node.mjs +19 -0
  16. package/dist/_entries/service-worker.d.mts +6 -0
  17. package/dist/_entries/service-worker.mjs +16 -0
  18. package/dist/_utils.mjs +240 -0
  19. package/dist/cache.mjs +599 -0
  20. package/dist/cache2.mjs +50 -0
  21. package/dist/cors.mjs +292 -0
  22. package/dist/docs/0.guide/0.index/index.md +117 -0
  23. package/dist/docs/0.guide/1.basics/0.lifecycle.md +68 -0
  24. package/dist/docs/0.guide/1.basics/1.routing.md +167 -0
  25. package/dist/docs/0.guide/1.basics/2.middleware.md +97 -0
  26. package/dist/docs/0.guide/1.basics/3.handler.md +165 -0
  27. package/dist/docs/0.guide/1.basics/4.response.md +171 -0
  28. package/dist/docs/0.guide/1.basics/5.error.md +117 -0
  29. package/dist/docs/0.guide/1.basics/6.nested-apps.md +57 -0
  30. package/dist/docs/0.guide/2.rules.md +698 -0
  31. package/dist/docs/0.guide/3.api/0.h3.md +144 -0
  32. package/dist/docs/0.guide/3.api/1.h3event.md +160 -0
  33. package/dist/docs/0.guide/4.advanced/0.plugins.md +50 -0
  34. package/dist/docs/0.guide/4.advanced/1.websocket.md +176 -0
  35. package/dist/docs/0.guide/4.advanced/2.nightly.md +13 -0
  36. package/dist/docs/1.utils/0.index/index.md +46 -0
  37. package/dist/docs/1.utils/1.request.md +447 -0
  38. package/dist/docs/1.utils/2.response.md +172 -0
  39. package/dist/docs/1.utils/3.cookie.md +33 -0
  40. package/dist/docs/1.utils/4.security.md +175 -0
  41. package/dist/docs/1.utils/5.proxy.md +57 -0
  42. package/dist/docs/1.utils/6.mcp.md +75 -0
  43. package/dist/docs/1.utils/7.more.md +117 -0
  44. package/dist/docs/1.utils/8.community.md +48 -0
  45. package/dist/docs/2.examples/0.index/index.md +17 -0
  46. package/dist/docs/2.examples/1.handle-cookie.md +67 -0
  47. package/dist/docs/2.examples/2.handle-query.md +76 -0
  48. package/dist/docs/2.examples/3.handle-session.md +210 -0
  49. package/dist/docs/2.examples/4.serve-static-assets.md +66 -0
  50. package/dist/docs/2.examples/5.stream-response.md +76 -0
  51. package/dist/docs/2.examples/6.validate-data.md +193 -0
  52. package/dist/docs/3.migration/0.index/index.md +204 -0
  53. package/dist/docs/README.md +37 -0
  54. package/dist/h3.d.mts +1669 -0
  55. package/dist/h3.mjs +1809 -0
  56. package/dist/index.d.mts +1634 -0
  57. package/dist/match.d.mts +123 -0
  58. package/dist/middleware.mjs +123 -0
  59. package/dist/normalize.mjs +645 -0
  60. package/dist/path.mjs +42 -0
  61. package/dist/proxy.mjs +254 -0
  62. package/dist/response.mjs +465 -0
  63. package/dist/rules/cache.d.mts +29 -0
  64. package/dist/rules/cache.mjs +163 -0
  65. package/dist/rules/compiler.d.mts +94 -0
  66. package/dist/rules/compiler.mjs +173 -0
  67. package/dist/rules/index.d.mts +77 -0
  68. package/dist/rules/index.mjs +34 -0
  69. package/dist/rules/proxy.d.mts +3 -0
  70. package/dist/rules/proxy.mjs +14 -0
  71. package/dist/tracing.d.mts +33 -0
  72. package/dist/tracing.mjs +89 -0
  73. package/package.json +148 -0
@@ -0,0 +1,698 @@
1
+ # Route Rules
2
+
3
+ > Add headers, redirects, CORS, caching, and proxying to groups of routes with one configuration object.
4
+
5
+ Route rules let you configure behavior that often lives in a CDN or reverse proxy. Instead of writing separate middleware for each concern, describe **what should happen** for each URL pattern:
6
+
7
+ ```ts
8
+ routeRules({
9
+ "/old/**": { redirect: "/new/**" },
10
+ "/assets/**": { headers: { "cache-control": "s-maxage=31536000" } },
11
+ "/api/**": { cors: true },
12
+ });
13
+ ```
14
+
15
+ Add `routeRules()` as global middleware. For each request, it:
16
+
17
+ 1. Finds every matching pattern.
18
+ 2. Merges the matching rules.
19
+ 3. Runs their middleware in the documented [execution order](#execution-order). Depending on the rule, it may wrap the response or answer the request directly.
20
+ 4. Makes the merged rules available as `event.context.routeRules`.
21
+
22
+ > [!TIP]
23
+ Route rules run inside your app, so they behave consistently across all runtimes supported by h3. You can still put a CDN in front of the app. Because the configuration is plain data, frameworks and build tools can also use it. See [Data-Only Rules](#data-only-rules) and the [compiler](#build-time-compiler).
24
+
25
+ Import the core feature from `h3/rules`. Caching and proxying have optional handlers in `h3/rules/cache` and `h3/rules/proxy`. Build-time code generation is available from `h3/rules/compiler`.
26
+
27
+ ## Quick Start
28
+
29
+ ```ts [server.mjs]
30
+ import { H3, serve } from "h3";
31
+ import { routeRules } from "h3/rules";
32
+ import { cache } from "h3/rules/cache"; // Needed for `cache` and `swr` (requires ocache)
33
+ import { proxy } from "h3/rules/proxy"; // Needed for `proxy`
34
+
35
+ const app = new H3();
36
+
37
+ app.use(
38
+ routeRules(
39
+ {
40
+ "/blog/**": { swr: 60 },
41
+ "/old/**": { redirect: { to: "/new/**", status: 301 } },
42
+ "/api/proxy/**": { proxy: "https://example.com/**" },
43
+ "/assets/**": { headers: { "cache-control": "s-maxage=31536000" } },
44
+ "/api/**": { cors: true },
45
+ "GET /api/cached/**": { swr: 60 }, // applies to GET only
46
+ },
47
+ { handlers: { cache, proxy } },
48
+ ),
49
+ );
50
+
51
+ serve(app);
52
+ ```
53
+
54
+ <read-more></read-more>
55
+
56
+ ## Built-in Rules
57
+
58
+ Start with `headers`, `redirect`, or `cors`: these work without extra dependencies. Register the cache handler for `cache` and `swr`, or the proxy handler for `proxy`.
59
+
60
+ | Rule | What it does |
61
+ | --- | --- |
62
+ | `headers` | Set response headers. |
63
+ | `redirect` | Send a server-side redirect. |
64
+ | `cors` | Handle CORS with [`handleCors`](/utils/security#handlecorsevent-options). Preflight (`OPTIONS`) requests are answered directly. |
65
+ | `cache` | Cache the matched route handler's response. Opt-in, see [Caching](#caching). |
66
+ | `swr` | Shortcut for `cache: { swr: true, maxAge?: number }`. |
67
+ | `proxy` | Forward the request to another origin or an in-app path. Opt-in, see [Proxying](#proxying). |
68
+
69
+ > [!NOTE]
70
+ > Route rules intentionally do not include an `auth` rule. Authentication needs executable logic that can check your user store, so it belongs in middleware. Use [`basicAuth`](/utils/security#basicauthopts) or your own middleware. If you create an auth rule with a [custom handler](#custom-rule-handlers), read [Security](#security) first, especially the `restricting` flag.
71
+
72
+ ### `headers`
73
+
74
+ Sets headers on the **final** response. This happens after `cache`, `redirect`, and `proxy`, so a `cache-control` value here overrides one produced by the cache handler.
75
+
76
+ ```ts
77
+ routeRules({
78
+ "/assets/**": { headers: { "cache-control": "s-maxage=31536000" } },
79
+ });
80
+ ```
81
+
82
+ ### `redirect`
83
+
84
+ Pass a string to use the default `307` status. Pass `{ to, status }` to choose the status:
85
+
86
+ ```ts
87
+ routeRules({
88
+ "/old/**": { redirect: "/new/**" }, // /old/a?x=1 → /new/a?x=1 (307)
89
+ "/moved/**": { redirect: "/new?from=**" }, // /moved/a/b → /new?from=a/b
90
+ "/search": { redirect: "https://example.com/s?lang=en" }, // /search?q=h3 → …/s?lang=en&q=h3
91
+ "/legacy": { redirect: { to: "/", status: 301 } },
92
+ });
93
+ ```
94
+
95
+ Redirects and [proxies](#proxying) share two useful behaviors:
96
+
97
+ - **Wildcard tails:** When the pattern ends in `/**`, a `**` in the target is replaced with the matched part of the path. A trailing `to: "/new/**"` appends it; a `**` anywhere else in the target's path, query, or fragment interpolates it in place (`/new/**/edit`, `/new?from=**`). An empty tail — a request to exactly the pattern's base — substitutes an empty string. In a query or fragment value the tail is percent-encoded so it cannot add parameters; in a path position it is forwarded byte for byte. The tail can never change the target's origin: a `**` that could name the destination host (`"**"`, `"**.cdn.example/x"`, `"https://**.example.com"`) is rejected at startup, and a request whose tail would still move the origin gets a `400`. A target `**` is left literal when no `/**` pattern applies to the route.
98
+ - **Query forwarding:** h3 preserves the request query string, including duplicate keys and encoding. If the target already has a query, the request query is appended to it.
99
+ A `redirect` never fires when the request is already at a URL it could have sent it to, so a target inside its own pattern does not loop and needs no `false` reset:
100
+
101
+ ```ts
102
+ routeRules({
103
+ "/docs/**": { redirect: "/docs/v2/**" }, // /docs/intro → /docs/v2/intro; /docs/v2/** is left alone
104
+ "/blog/**": { redirect: "/blog/**.md" }, // /blog/a/b → /blog/a/b.md; any /blog/**.md path is left alone
105
+ });
106
+ ```
107
+
108
+ Only the target's path is compared, using the literal text before its first and after its last `**`; a target with no `**` in its path (`/new`, `/new?from=**`) is skipped only on that exact path. Absolute URL targets, a `**` in the middle of a segment (`/docs/v2-**`), and a bare `"/**"` target (which matches every path) always redirect.
109
+
110
+ ### `cors`
111
+
112
+ Pass `true` to use permissive defaults. Pass a `CorsOptions` object to configure an origin allowlist, credentials, `maxAge`, and other options:
113
+
114
+ ```ts
115
+ routeRules({
116
+ "/api/**": { cors: { origin: ["https://example.com"], credentials: true } },
117
+ });
118
+ ```
119
+
120
+ > [!NOTE]
121
+ > Internally, `cors: true` becomes an empty options object (`{}`). On a more specific pattern, it inherits options from broader matching patterns instead of resetting them to permissive defaults. Use `cors: false` to remove inherited CORS behavior.
122
+
123
+ ### `cache` and `swr`
124
+
125
+ Caches the response from the matched route handler. You must register a cache handler first. See [Caching](#caching).
126
+
127
+ `swr: 60` is shorthand for `cache: { swr: true, maxAge: 60 }`. A value of `0` is valid. Use `swr: false` to remove an inherited `cache` rule.
128
+
129
+ ### `proxy`
130
+
131
+ Forward matching requests elsewhere. This rule needs the opt-in handler from `h3/rules/proxy` — see [Proxying](#proxying).
132
+
133
+ ## How Matching Works
134
+
135
+ Route rules use [🌳 Rou3](https://github.com/h3js/rou3), the same engine as [routing](/guide/basics/routing). Patterns are matched against `event.url.pathname`.
136
+
137
+ Route matching and rule matching differ in one important way: a route uses only its most specific match, but **route rules apply every matching pattern**. H3 merges matches from least specific to most specific. Object options are shallow-merged, with the more specific values winning. Primitive values and other non-object values are replaced completely.
138
+
139
+ ```ts
140
+ routeRules({
141
+ "/**": { headers: { "x-app": "demo" } },
142
+ "/api/**": { headers: { "x-api": "1" } },
143
+ });
144
+
145
+ // GET /api/users → x-app: demo, x-api: 1
146
+ ```
147
+
148
+ ### Resetting a Rule
149
+
150
+ Set a rule to `false` on a more specific pattern to turn off inherited behavior for that part of your app:
151
+
152
+ ```ts
153
+ routeRules({
154
+ "/api/**": { cors: { origin: ["https://example.com"] } },
155
+ "/api/public/**": { cors: false }, // no CORS handling under /api/public
156
+ });
157
+ ```
158
+
159
+ ### Method-Scoped Rules
160
+
161
+ Add an HTTP method before a pattern when a rule should apply only to that method. Patterns without a method apply to every method. Method-specific rules merge last, so they can override general rules. Method names are case-insensitive.
162
+
163
+ ```ts
164
+ routeRules({
165
+ "/api/**": { headers: { "x-api": "1" } }, // any method
166
+ "GET /api/**": { swr: 60 }, // GET only
167
+ });
168
+ ```
169
+
170
+ H3 groups equivalent route patterns together. For example, `/users/:id` and `/users/:userId` describe the same route group, as do `/users/*` and `/users/**`. A general rule using one spelling can therefore merge with a method-specific rule using another spelling.
171
+
172
+ > [!NOTE]
173
+ > If spellings in one route group have different specificity, such as `/a/**` and `/a/**:rest`, the more specific pattern resolves last and wins. This applies whether or not the rules are method-scoped. To avoid surprising results, use one spelling consistently.
174
+
175
+ ### Reading Matched Rules
176
+
177
+ Handlers and middleware can read the final merged configuration from `event.context.routeRules`. Entries are keyed by rule name. More specific values are already merged, and shortcuts such as `swr` are already expanded:
178
+
179
+ ```ts
180
+ app.get("/blog/:slug", (event) => {
181
+ const rules = event.context.routeRules;
182
+ rules?.cache; // { swr: true, maxAge: 60 }
183
+ rules?.redirect?.to; // "/new"
184
+ rules?.headers?.["x-a"]; // "1"
185
+ });
186
+ ```
187
+
188
+ > [!IMPORTANT]
189
+ > Match results are [memoized](#memoization) by default. This means the same result object can be shared by multiple requests. Always treat `event.context.routeRules` and its nested values as **read-only**.
190
+
191
+ The context contains the merged values, but not the patterns they came from. Framework integrations that need the contributing pattern and its parameters can use a [matcher](#using-matchers-directly) directly:
192
+
193
+ ```ts
194
+ const { routeRules, matchedRules } = matcher("GET", "/blog/post");
195
+ routeRules.cache; // { swr: true, maxAge: 60 } — same object the context gets
196
+ matchedRules.cache?.route; // "/blog/**"
197
+ matchedRules.cache?.params; // rou3 params of the contributing patterns
198
+ ```
199
+
200
+ You can register `routeRules()` more than once. Each instance merges its results over earlier instances, and the later instance wins for the same rule name. This lets a framework provide defaults while an app adds or overrides its own rules.
201
+
202
+ ### Data-Only Rules
203
+
204
+ A rule without a registered handler is **data-only**. It is still matched, merged, and exposed through `event.context.routeRules`, but it does not change the response at runtime. Frameworks and build tools can use data-only keys such as `prerender`, `isr`, or custom metadata.
205
+
206
+ ```ts
207
+ routeRules({
208
+ "/docs/**": { prerender: true },
209
+ });
210
+
211
+ // event.context.routeRules.prerender → true
212
+ ```
213
+
214
+ Declare custom data-only keys to make them type-safe. See [TypeScript](#typescript).
215
+
216
+ ## Caching
217
+
218
+ The core `h3/rules` package does not include a cache implementation. To use `cache` or `swr`, register a `cache` handler. H3 throws while creating the matcher if these rules are present without a handler.
219
+
220
+ H3 provides an optional handler backed by [ocache](https://github.com/unjs/ocache) in `h3/rules/cache`. Install `ocache` alongside `h3`; it is an optional peer dependency. Apps that do not use caching will not include ocache in their bundles.
221
+
222
+ ```ts
223
+ import { routeRules } from "h3/rules";
224
+ import { cache } from "h3/rules/cache";
225
+
226
+ // default: in-memory storage
227
+ app.use(routeRules({ "/blog/**": { swr: 60 } }, { handlers: { cache } }));
228
+ ```
229
+
230
+ The default handler uses in-memory storage. Create your own handler instance to change the storage or default options:
231
+
232
+ ```ts
233
+ import { createOcacheRuleHandler } from "h3/rules/cache";
234
+
235
+ app.use(
236
+ routeRules(rules, {
237
+ handlers: {
238
+ cache: createOcacheRuleHandler({
239
+ storage: myStorage, // ocache storage instance (or a factory), shared by every rule
240
+ defaults: { staleMaxAge: 60 }, // ocache defaults incl. hooks (rule options win)
241
+ }),
242
+ },
243
+ }),
244
+ );
245
+ ```
246
+
247
+ ### How Entries Are Keyed
248
+
249
+ The `cache` rule wraps the **matched route handler**, so it only runs when h3 finds a route. By default, entries use the `"h3/route-rules"` group and the name `<handlerScope>:<method>:<rulePattern>:<matchedRoute>`:
250
+
251
+ - The **scope** is unique to both the handler instance and the matched route handler. Two apps or matchers cannot read or write each other's entries, even if they share the module-level `cache` export.
252
+ - The **method** prevents a body-less `HEAD` response from being stored as the `GET` response.
253
+ - The **rule pattern** is the normalized key, spelled the way h3 stores the route: `"/café/**"` and `"/a b/**"` appear as `/caf%C3%A9/**` and `/a%20b/**`. See [Encoded and Alternate Path Spellings](#encoded-and-alternate-path-spellings).
254
+ You can override `group` and `name` as normal `cache` rule options. Be careful: an explicit `name` replaces the entire default name, including its isolation.
255
+
256
+ The generated scope changes between processes. With persistent storage, each process therefore creates its own entries, and workers do not share them. For stable keys, pass `createOcacheRuleHandler({ id: "my-app" })`, but only use that instance for one app.
257
+
258
+ > [!NOTE]
259
+ > Normally, ocache gives each cached handler its own storage instance. `createOcacheRuleHandler` instead shares one store across all of its rules. This is either the `storage` you provide or a memory store created on the first cached request. The result is one bounded cache for all routes in the app. Two instances with the same `id` also share that default store because their keys match.
260
+
261
+ ### What Reaches a Cached Handler
262
+
263
+ H3 only passes request data to the cached handler when that data is represented in the cache key. This prevents cached responses from depending on values that do not vary the entry:
264
+
265
+ - **Query strings are removed by default.** The handler receives a URL without a query, and the query does not affect the key. Use `allowQuery: ["page", "q"]` to allow specific names, or `allowQuery: true` to include the full query string.
266
+ - **Headers are removed unless listed in `varies`.** See [Credentials and Cookies](#credentials-and-cookies) for additional credential rules. Conditional, tracing, and request-ID headers are also removed; read them in middleware outside the cache rule.
267
+ - A response is **returned but not stored** if it uses a `Vary` header that the key does not cover, sets `Cache-Control: no-store`, `private`, or `no-cache`, has a status other than `200`, `203`, `301`, or `308`, or is larger than `maxBodySize`.
268
+ Resolving an entry has a **30-second deadline**, controlled by `maxResolveTime`. When the deadline expires, all waiters are rejected and the entry is evicted. The handler's `event.req.signal` is aborted too. Forward this signal if the handler makes an upstream `fetch` request.
269
+
270
+ ### Credentials and Cookies
271
+
272
+ > [!IMPORTANT]
273
+ > H3 removes `Cookie`, `Authorization`, and `Proxy-Authorization` before calling a cached handler. These headers do not vary the automatically generated key. Without this protection, a response rendered for one user could be cached under an anonymous key, served to other users, and marked `public, s-maxage=N` for shared caches.
274
+
275
+ - Set `cache: { allowAuthorization: true }` to pass the authorization credential to the handler. H3 hashes it into the key and adds it to `Vary`, so each credential receives a separate entry.
276
+ - Some runtimes may provide immutable headers and a request that cannot be rebuilt. If h3 cannot remove credentials safely, it returns a `500` instead of caching a credentialed response under a credential-free key.
277
+ - Headers listed in `varies` remain visible to the cached handler. Each value gets its own key and is added to the response's `Vary` header. Credentials are an exception: listing them in `varies` does not forward them. Use `allowAuthorization` instead.
278
+ - A handler's `Set-Cookie` header is sent only to the request that produced it and is **never stored in the cache**. This prevents one visitor's session cookie from being replayed to others. `allowCookies` only controls the request: it selects which cookie values reach the handler and vary the entry. Do not cache a route that must set a cookie on every response.
279
+
280
+ ### Cache-Control Behavior
281
+
282
+ - H3 preserves a handler's `Cache-Control` header when it contains `private` or `no-store`. Ocache also refuses to store the response.
283
+ - H3 replaces any other handler-provided `Cache-Control` value with the rule's generated `public, max-age=N, s-maxage=N`. Use a `headers` rule when you need full control over the final value.
284
+ - Set `sendCacheControl: false` to disable the generated header.
285
+
286
+ ### Bring Your Own Cache
287
+ You can use another cache implementation instead of ocache. Create a handler with the core factory. `defineCachedHandler` receives the matched route handler and merged rule options, with `group` and `name` already filled in, and returns a cached wrapper. Frameworks such as Nitro can integrate here:
288
+
289
+ ```ts
290
+ import { createCacheRuleHandler } from "h3/rules";
291
+
292
+ const cache = createCacheRuleHandler({
293
+ defineCachedHandler: (handler, opts) => myCachedHandler(handler, opts),
294
+ });
295
+ ```
296
+
297
+ The declarative options in `RouteRuleConfig["cache"]` use h3's ocache-compatible `CacheRuleOptions` schema. Implementation hooks such as `getKey`, `shouldCache`, and `getMaxAge` are not rule data. Pass them through the handler factory's `defaults` instead.
298
+
299
+ ## Proxying
300
+
301
+ Proxying is also **opt-in**. The handler uses [`proxyRequest`](/utils/proxy#proxyrequestevent-target-opts), so it is exported separately from `h3/rules/proxy`. Apps that do not proxy will not include it in their bundles. Register the handler explicitly; h3 throws while creating the matcher if a `proxy` rule has no handler:
302
+
303
+ ```ts
304
+ import { routeRules } from "h3/rules";
305
+ import { proxy } from "h3/rules/proxy";
306
+
307
+ app.use(
308
+ routeRules({ "/api/proxy/**": { proxy: "https://example.com/**" } }, { handlers: { proxy } }),
309
+ );
310
+ ```
311
+
312
+ Proxy targets behave like [`redirect`](#redirect) targets: h3 substitutes matching `/**` tails and forwards the query string.
313
+
314
+ > [!TIP]
315
+ > You can keep `cache` or `proxy` as data-only rules. Pass `handlers: { cache: undefined }` or `handlers: { proxy: undefined }`. The rule will still be matched and exposed on the context, but it will not run any behavior.
316
+
317
+ ## Execution Order
318
+
319
+ Rules that have runtime handlers run as middleware. Lower order numbers run first and wrap the rules inside them:
320
+
321
+ ```
322
+ cors (-3) → [-2 free] → headers (-1) → custom rules (0) → redirect (1) → proxy (2) → cache (3) → route handler
323
+ ```
324
+
325
+ In practice:
326
+
327
+ - CORS can answer a preflight request before any other rule runs.
328
+ - `headers` wraps every rule inside it, so its values override headers produced by caching or other inner rules.
329
+ - `redirect`, `proxy`, and `cache` can finish the request without calling the next rule. Each has a separate order. `cache` is innermost and dispatches the route handler.
330
+ - Because `cache` dispatches the route itself, it also ends the app's <u>global</u> middleware chain for every request it can cache. See [Cached Routes and Global Middleware](#cached-routes-and-global-middleware).
331
+ - Custom rules use order `0` by default, so they run before `redirect`, `proxy`, and `cache`.
332
+ `order` is a number, and lower values run first. The `-2` slot is intentionally free for a custom rule that must short-circuit before `headers`, `redirect`, `proxy`, and `cache`. Rules with the same order run by rule name. That order is deterministic but has no semantic meaning, so give any short-circuiting handler an explicit order. See [Security](#security) for the security implications.
333
+
334
+ ## Options
335
+
336
+ Pass these options as the second argument to `routeRules(config, options)`:
337
+
338
+ | Option | Description |
339
+ | --- | --- |
340
+ | `baseURL` | Prefix every rule pattern (trailing slash trimmed). |
341
+ | `handlers` | Add or override rule handlers by name. `undefined` makes that rule data-only. |
342
+ | `memoize` | Memoize match results per `method + pathname`. Enabled by default, see [Memoization](#memoization). |
343
+ | `preMerge` | Resolve each pattern's subsumption chain at startup, see [Pre-merging](#pre-merging). |
344
+
345
+ ## Custom Rule Handlers
346
+
347
+ Use a custom handler when you need runtime behavior that is not built in. A handler definition has the shape `{ handler, order? }`:
348
+
349
+ - `handler` turns a matched rule into H3 [middleware](/guide/basics/middleware).
350
+ - `order` controls when it runs. Lower values run first and the default is `0`. Built-in rules use `-3` through `-1` and `1` through `3`; see [Execution Order](#execution-order).
351
+ The handler receives `{ options, route, params?, handler? }`. `options` contains the merged value. `route` is the most specific contributing pattern, which is provenance not available on `event.context.routeRules`.
352
+
353
+ ```ts
354
+ app.use(
355
+ routeRules(
356
+ { "/x/**": { shout: "hello" } },
357
+ {
358
+ handlers: {
359
+ shout: {
360
+ handler: (matched) => (event) => {
361
+ event.res.headers.set("x-shout", String(matched.options).toUpperCase());
362
+ },
363
+ },
364
+ },
365
+ },
366
+ ),
367
+ );
368
+ ```
369
+
370
+ > [!IMPORTANT]
371
+ > A custom handler that **restricts** access (an auth gate, a rate limit, an IP allowlist) must also set `restricting: true` — see [Security](#security) for why.
372
+
373
+ ## Performance
374
+
375
+ ### Memoization
376
+
377
+ A given `method + pathname` always produces the same merged result, so `routeRules()` memoizes results **by default**. Repeated requests can skip pattern lookup, path canonicalization, merging, and middleware construction, reducing the hot path to a map lookup.
378
+
379
+ - The memoization map holds up to `1024` entries by default. Dynamic paths therefore cannot grow it without limit. Change the cap with `memoize: { max }`.
380
+ - Eviction uses [SIEVE](https://cachemon.github.io/SIEVE-website/): entries are evicted in insertion order, except that an entry requested since the eviction hand last passed it survives that pass. A small set of hot paths is therefore not displaced by a flood of one-shot dynamic paths, which plain FIFO would evict it alongside. A cache hit stays a single map lookup — unlike LRU, nothing is reordered on read.
381
+ - Use `memoize: false` to resolve every request again and create fresh result objects.
382
+ Lower-level matchers do not enable memoization automatically. Wrap a matcher with `memoizeRouteRulesMatcher(matcher, opts?)` to opt in. If you do not use it, bundlers can tree-shake the memoization code.
383
+
384
+ ### Pre-merging
385
+
386
+ Set `preMerge: true` to merge each pattern's inheritance chain ahead of time, either when the matcher starts or at build time with the [compiler](#build-time-compiler). Each request then resolves only the most specific layer instead of merging every matched layer. Method-specific rules, general rules, `false` resets, and per-rule `params` behave the same as they do with normal per-request merging.
387
+
388
+ Pre-merging only works for rule sets whose relationships can be determined in advance. It cannot safely analyze partial overlaps such as `/a/:x/c` and `/a/b/:y`, where the most specific match is ambiguous, or patterns with regex parameters:
389
+
390
+ - The **runtime matcher throws during startup**.
391
+ - The **compiler falls back safely**. It logs a warning and uses normal compilation, so the generated matcher remains correct.
392
+
393
+ ## Using Matchers Directly
394
+ Most apps should use `routeRules()`. Frameworks that need to resolve rules outside middleware can use the lower-level exports directly:
395
+
396
+ ```ts
397
+ import { createRouteRulesMatcher, normalizeRouteRules, memoizeRouteRulesMatcher } from "h3/rules";
398
+ import { cache } from "h3/rules/cache";
399
+
400
+ const matcher = memoizeRouteRulesMatcher(
401
+ createRouteRulesMatcher(normalizeRouteRules(config), {
402
+ baseURL: "/base",
403
+ preMerge: true,
404
+ handlers: { cache },
405
+ }),
406
+ );
407
+
408
+ const { routeRules, routeRuleMiddleware } = matcher("GET", "/blog/post");
409
+ ```
410
+
411
+ - `normalizeRouteRules()` expands shortcuts such as `swr`, normalizes string and boolean forms, and canonicalizes keys. `createRouteRulesMatcher()` expects these **normalized** rules, unlike `routeRules()`. This keeps normalization code out of runtime bundles that do not need it.
412
+ - A match returns `{ routeRules, matchedRules, routeRuleMiddleware }`: the merged values placed on the event context, those values with pattern provenance, and the ordered middleware chain.
413
+ - `mergeMatchedRouteRules()` is the pure merge operation: it accepts matched layers and returns matched rules. `ruleHandlers` is the default registry for `headers`, `redirect`, and `cors`.
414
+
415
+ ## TypeScript
416
+ Two interfaces describe route rules. `RouteRuleConfig` types the configuration you **write**, while `RouteRules` types the values produced by matching and merging. Declare a custom rule with the same shape in both interfaces:
417
+
418
+ ```ts
419
+ declare module "h3/rules" {
420
+ interface RouteRuleConfig {
421
+ /** Incremental Static Regeneration (handled at build time). */
422
+ isr?: number | boolean;
423
+ /** Add this route to the prerender queue. */
424
+ prerender?: boolean;
425
+ /** A data-only rule with no runtime handler. */
426
+ audience?: "public" | "internal";
427
+ }
428
+ interface RouteRules {
429
+ isr?: number | boolean;
430
+ prerender?: boolean;
431
+ audience?: "public" | "internal";
432
+ }
433
+ }
434
+
435
+ // event.context.routeRules.audience → "public" | "internal" | undefined
436
+ ```
437
+
438
+ Key points:
439
+
440
+ - `RouteRuleConfig` is **closed**. Unknown keys cause type errors, so TypeScript catches a typo such as `redirct`. Module augmentation adds your custom keys.
441
+ - `RouteRules` types merged values everywhere they appear: `event.context.routeRules`, the `routeRules` returned by a matcher, and each matched rule's `options` passed to a handler.
442
+ - Data-only rules pass through normalization and merging unchanged. Module augmentation affects only their types.
443
+ - H3 owns the `RouteRules` interface and re-exports it from `h3/rules`. Augmenting `declare module "h3"` therefore updates the same declaration used by Nitro and the standalone `h3-rules` package. `RouteRuleConfig` exists only in `h3/rules`.
444
+
445
+ ### Redeclaring Built-in Rules
446
+ Frameworks can also redeclare a **built-in** key when they provide a different rule shape. The augmented type replaces h3's type for that key:
447
+
448
+ ```ts
449
+ declare module "h3" {
450
+ interface RouteRules {
451
+ redirect?: string | { to: string; status?: number };
452
+ cors?: boolean;
453
+ }
454
+ }
455
+ ```
456
+
457
+ The replacement can use any shape, including primitives and `false`. `RouteRules` is unconstrained. Built-in definitions live separately in `BuiltinRouteRules` and are added only for keys that have not been redeclared. This combined context type is exported as `ResolvedRouteRules`. A redeclaration **replaces** the built-in type instead of intersecting with it. Built-in keys you do not redeclare keep their exact option types, so expressions such as `rules.redirect?.to` do not require extra narrowing.
458
+
459
+ > [!NOTE]
460
+ > h3 composes in only its own built-ins and declares no index signature. Contributing a blanket `[key: string]: unknown` to the shared interface would turn every other module's augmentation into a type error — which is why an undeclared data-only key is readable at runtime but not typed until you declare it.
461
+
462
+ Two more types are exported for integrations: `NormalizedRouteRules` (one pattern's rules after `normalizeRouteRules()` — `RouteRules` plus `false` resets and arbitrary names) and `MatchedRouteRule` (a merged rule with its provenance, what a rule handler receives).
463
+
464
+ ## Security
465
+
466
+ The matcher protects against several path and ordering edge cases by default. Most apps do not need extra configuration. Read this section carefully if a custom rule restricts access.
467
+
468
+ ### Encoded and Alternate Path Spellings
469
+
470
+ H3 matches rules against **every meaningful interpretation of the request path**, not only the spelling used for route dispatch. Otherwise, an attacker could bypass a rule by encoding the same path differently.
471
+
472
+ [`event.url.pathname`](/guide/api/h3event#pathname-encoding) decodes an escape only when the decoded character survives URL serialization. For example, `/%40admin` is already served as `/@admin`. Other values remain encoded, including separators (`%2f`, `%5c`), `%25` at any nesting depth, values the serializer would encode again (`%20` and non-ASCII characters), and C0 controls.
473
+
474
+ Route patterns, however, are normally written with the character itself. To prevent encoded paths from bypassing those patterns, h3 also resolves each request against:
475
+
476
+ - its **canonical** reading (encoded separators decoded, `.` / `..` resolved),
477
+ - its **slash-merged** reading (what a downstream like nginx `merge_slashes` resolves),
478
+ - its **percent-decoded** reading,
479
+ - its **uppercase-hex** and **lowercase-hex** spellings, since hex case is not meaningful in a URL ([RFC 3986 §6.2.2.1](https://www.rfc-editor.org/rfc/rfc3986#section-6.2.2.1)).
480
+
481
+ ```ts
482
+ routeRules({
483
+ "/@admin/**": { redirect: "/elsewhere" },
484
+ "/a admin/**": { redirect: "/elsewhere" },
485
+ });
486
+
487
+ // GET /@admin/data → matched
488
+ // GET /%40admin/data → matched (h3 serves it as /@admin/data)
489
+ // GET /a%20admin/data → matched (a proxied backend would serve it as "/a admin/data")
490
+ // GET /a%2520admin/x → matched (…and so would one that decodes twice)
491
+ // GET /admin%2fpanel → matched against /admin/panel too
492
+ ```
493
+ An alternate interpretation can add a rule or override it only with an equally or more specific pattern. A crafted path can never use a broader pattern to weaken the rule selected for the served path.
494
+
495
+ Rule keys are normalized at configuration time exactly like h3 route patterns, so a rule matches every request that the route registered with the same string serves:
496
+
497
+ - Escapes for characters that survive URL serialization are decoded, as in every request path. `"/%40admin/**"` is `/@admin/**`. An encoded Rou3 metacharacter therefore becomes a metacharacter: `"/a/%3Aid"` is the `:id` parameter pattern, and `"/f/%2A%2A"` is a catch-all, just like `app.get("/a/%3Aid")`.
498
+ - Raw characters that the URL serializer would encode are encoded: `"/a admin/**"` and `"/a%20admin/**"` are the same pattern, as are `"/café/**"` and `"/caf%C3%A9/**"`. This applies inside a regex constraint too: `"/(café|tea)/**"` becomes `/(caf%C3%A9|tea)/**`, because a constraint is matched against the encoded request path.
499
+ - `.` and `..` segments are resolved.
500
+ - Every other escape stays exactly as written and matches literally. This includes `%2f`, `%5c`, and `%25`, and also `%7B`, `%7D`, and `%3F`, which would otherwise become Rou3 group or optional syntax. For example, `"/%7Bq%7D/a"` matches the literal `/%7Bq%7D/a` request, just like `app.get("/%7Bq%7D/a")`, and never `/q/a`. Write the raw character (`"/a{b}"`) for the syntax. An escape inside a regex constraint is literal regex text (`[%5e]` is the class of `%`, `5`, and `e`).
501
+ The hex-case spellings of a request are ordinary alternate interpretations: they can add a rule, but they never apply a `false` reset. A key spelled in lowercase hex (`"/caf%c3%a9/**"`) therefore also adds its rules to `/café/x`, but its `false` reset only applies to requests spelled like the key. To exempt a route from a broader rule with a reset, spell the key the way you spell the route, or use the raw character (`"/café/**"`). A pattern whose escapes mix hex cases can come from a `baseURL` in the other case than the key, or from normalization, which encodes a raw character in uppercase next to a lowercase escape (`"/é/caf%c3%a9/**"` becomes `/%C3%A9/caf%c3%a9/**`). Text that Rou3 encodes itself when it registers a pattern, such as a literal `^`, the escapes `\{`, `\}`, and `\?`, or raw characters in a `baseURL`, is always encoded in uppercase, so it counts as uppercase hex here. No recased spelling of a request can reproduce that mix, so a mixed pattern without a regex constraint is registered with all its hex in uppercase. Every spelling then matches it, and a gate applies to all of them. On the exact mixed spelling the rule acts like an alternate interpretation: it can add a rule, but its `false` reset or narrower permission does not override a broader rule there. A mixed pattern with a regex constraint (`"/bä/(x%c3%a9)/**"` becomes `/b%C3%A4/(x%c3%a9)/**`) is kept exactly as written, because recasing an escape would change its regex, so it only matches requests spelled in exactly that mix.
502
+
503
+ Dispatch is unaffected: routing still uses `event.url.pathname` as served, and `redirect` / `proxy` still forward the raw path bytes.
504
+
505
+ ### Resets and the `restricting` Flag
506
+
507
+ A `false` reset needs special handling across alternate path interpretations. Because a reset removes a rule, it would otherwise look the same as a rule that never matched, allowing a broader alternate interpretation to add it again. The matcher handles this based on whether the rule permits or restricts behavior:
508
+
509
+ - A rule that **permits** behavior stays reset, unless an alternate interpretation matches it with a pattern that is equal to or more specific than **every** pattern that reset it. That is the same pattern that would win over the reset on a single path. A broader or partially overlapping pattern never brings it back, so a crafted path cannot undo `cors: false` on a private subtree with a `/**` rule. All built-in rules (`cors`, `redirect`, `headers`, `cache`, and `proxy`) belong to this category.
510
+ - A rule that **restricts** behavior is added again. This fail-closed approach prevents an exemption for a single-segment pattern from carrying over to a decoded path with more segments.
511
+ Handlers declare which they are via `RuleHandler.restricting`, which defaults to `false`. No built-in sets it.
512
+
513
+ > [!IMPORTANT]
514
+ > A **custom** rule handler that restricts (an auth gate, a rate limit, an IP allowlist) must set `restricting: true`, or a `false` reset on one reading will exempt it on every other reading that no narrower pattern re-adds it on. The default is the safe choice for a permission and the wrong one for a restriction, and nothing warns. Rules with no handler at all (data-only rules a consumer such as Nitro acts on itself) cannot carry the flag — a consumer treating one as a gate needs its own reasoning at the point of use.
515
+
516
+ A narrower pattern wins over a broader reset on every reading, so an encoded separator cannot strip a rule that the decoded path is given:
517
+
518
+ ```ts
519
+ routeRules({
520
+ "/docs/**": { headers: false },
521
+ "/docs/x/**": { headers: { "content-security-policy": "default-src 'self'" } },
522
+ });
523
+
524
+ // GET /docs/x/y → CSP header
525
+ // GET /docs/x%2fy → CSP header (served as one segment, so only `/docs/**` matches it,
526
+ // but its decoded reading /docs/x/y matches `/docs/x/**`)
527
+ // GET /docs/a%2fb → no header (/docs/a/b is outside `/docs/x/**`)
528
+ ```
529
+
530
+ This applies in the loosening direction too. Alternate interpretations stand for what a decoding downstream, such as a proxy, serves, so the permission granted for that path applies even though h3 dispatches the request differently:
531
+
532
+ ```ts
533
+ routeRules({
534
+ "/api/**": { cors: false },
535
+ "/api/public/:file": { cors: { origin: ["https://public.example"] } },
536
+ });
537
+
538
+ // GET /api/public/secret → cors
539
+ // GET /api/public%2fsecret → cors, even when h3 dispatches it to an `/api/:id` handler,
540
+ // because the decoded reading is /api/public/secret
541
+ // GET /api/public%2fa%2fb → no cors (/api/public/a/b has one segment too many for `:file`)
542
+ ```
543
+
544
+ The same happens without any reset: `"/api/public/:file": { cors }` alone also applies to `/api/public%2fsecret`. It extends to encoded dot segments, since the canonical reading resolves them: `/api/x%2f..%2fpublic%2fy` is read as `/api/public/y` and gets the same CORS, credentials included when the rule sets `credentials: true` with an origin allowlist, while h3 dispatches it to `/api/:id`. If a handler must not answer an encoded separator with a permission written for the decoded path, reject the request in that handler or in middleware.
545
+
546
+ A reinstated `redirect` or `proxy` rule never forwards out of its scope. It removes the pattern's prefix from the served path, and that prefix has a different number of segments when the separator is encoded. So, as for an encoded path that reaches the rule without any reset, the request is rejected with `400` (see [Redirect and Proxy Target Safety](#redirect-and-proxy-target-safety)). With `{"/old/**": { redirect: false }, "/old/new/**": { redirect: { to: "https://example.com/n/**" } }}`, `/old/new%2fx` now gets that `400` instead of reaching the route handler.
547
+
548
+ Every pattern that reset a rule counts, in every interpretation, before any interpretation adds one. So the result never depends on the order in which they are checked. A rule set compiled with `preMerge` records each reset at the most specific pattern its interpretation matched, which is never broader than the real one. It may keep a rule reset where the default matcher reinstates it, but never the other way around. The [specificity guard](#specificity-guard) decides containment, and every case it cannot decide keeps the rule reset.
549
+
550
+ One residual, in the fail-safe direction: a reset permission comes back only from a pattern contained in the one that reset it, even when the other interpretation is a faithful re-spelling of the path that granted it, such as another hex-case spelling. `{"/**": { cors }, "/app/:page": { cors: false }}` keeps the exemption on `/app/a%2fb`, whose decoded reading has two segments and arguably falls outside the single-segment pattern that reset it. Every case this gets wrong errs toward <u>not</u> applying a permission.
551
+
552
+ ### `HEAD` and Preflight Requests
553
+
554
+ `HEAD` requests are served by the matching `GET` route ([RFC 9110](https://www.rfc-editor.org/rfc/rfc9110#name-head)), so `GET`-scoped rules apply to `HEAD` too — otherwise a rule keyed `GET /admin/**` could be dodged with a `HEAD` request. An explicit `HEAD` key still merges over (and can reset) the `GET` ones.
555
+
556
+ A CORS preflight arrives as `OPTIONS`, so a `cors` rule scoped to the method the browser announces in `Access-Control-Request-Method` is resolved for the preflight as well. Only the `cors` rule is taken from that lookup — never any other rule scoped to that method, since browsers send preflights without credentials and a gate lifted out of it would reject every preflight.
557
+
558
+ ### Headers on Short-Circuited Responses
559
+
560
+ `headers` sits at order `-1`, so it wraps every rule that runs inside it — but not one that runs <u>outside</u> it. A response produced by an outer rule (the `cors` preflight answer, or a custom handler in the free `-2` band) short-circuits before the headers middleware is entered, so the `headers` rule is not applied to it. Every other response goes through it, including error responses raised further in (a `404`, or a handler that throws).
561
+
562
+ If a header must be present on such a response too, set it from global middleware registered before `routeRules()`, and set it on `event.res.errHeaders` as well — an error response is built from that bag, not from `event.res.headers`.
563
+
564
+ ### Cached Routes and Global Middleware
565
+
566
+ The `cache` rule dispatches the matched route handler itself instead of calling the next layer, so **global middleware registered after `routeRules()` never runs for a cacheable request to a route a `cache` rule matched** — on a cache <u>miss</u> just as much as on a hit:
567
+
568
+ ```ts
569
+ app.use(routeRules({ "/api/**": { swr: 60 } }, { handlers: { cache } }));
570
+ app.use(requireAuth); // never runs for a cacheable /api/** request — not even the first
571
+ app.get("/api/private/:id", handler);
572
+ ```
573
+
574
+ Register `routeRules()` **after** every global middleware that has to run for cached routes:
575
+
576
+ ```ts
577
+ app.use(requireAuth); // runs first, for every request
578
+ app.use(routeRules({ "/api/**": { swr: 60 } }, { handlers: { cache } }));
579
+ ```
580
+
581
+ Per-route middleware is unaffected — it is part of the composed route handler the cache rule dispatches, so it runs on a miss and is cached along with the response (which is its own reason not to put a credential check there). `redirect` and `proxy` end the chain too, but they answer the request outright and never reach the route handler, so this is only surprising for `cache`.
582
+
583
+ Requests the cache never serves are the exception. The ocache handler passes anything it would not store — every method other than `GET` and `HEAD`, and any request carrying a `Range` header — down the chain instead, the way a CDN sends an uncacheable request to its origin. A `POST` to a `cache`-matched route therefore runs the middleware registered after `routeRules()`, reaches the route through normal dispatch, and keeps its `Authorization` header:
584
+
585
+ ```ts
586
+ app.use(routeRules({ "/api/**": { swr: 60 } }, { handlers: { cache } }));
587
+ app.use(requireAuth); // skipped for a cacheable GET — but runs for POST, PUT, ...
588
+ app.get("/api/non-cachable/:id", handler);
589
+ app.post("/api/non-cachable/:id", handler);
590
+ ```
591
+
592
+ A [custom cache handler](#custom-rule-handlers) declares its own uncacheable requests with the `shouldBypass` option of `createCacheRuleHandler`; without it, every request to a matched route ends the chain. A `shouldBypassCache` hook passed through `createOcacheRuleHandler({ defaults })` is resolved inside the cache instead, so it does not pass the request through.
593
+
594
+ That same composition is why `routeRules()` belongs in `app.use()`, not on a route:
595
+
596
+ ```ts
597
+ // Works, but the whole rule chain runs twice per request:
598
+ app.get("/api/x", handler, { middleware: [routeRules(rules, { handlers: { cache } })] });
599
+ ```
600
+
601
+ Registered this way (or composed in with `defineHandler({ middleware })`), the rule sits inside the very handler the `cache` rule dispatches, so the dispatch re-enters it. The rule detects the re-entry and continues to the route handler instead of dispatching a second time — the response and the caching are correct — but every other matched rule still runs on both passes. Keep `routeRules()` global.
602
+
603
+ ### Redirect and Proxy Target Safety
604
+
605
+ For a `/**` target, the matched tail is appended and the resulting path is checked against the target's own base; a request that would escape it (for example via an encoded `..%2f` traversal) is rejected with `400`.
606
+
607
+ H3 builds the tail by removing the rule pattern's prefix, counted in segments. That prefix must therefore contain the same number of segments for every matching request.
608
+
609
+ Some patterns can match a variable number of prefix segments. These include an optional parameter (`/:lang?/old/**`) or a group that spans a separator (`/x{/a}?/old/**`). H3 rejects these requests with `400` rather than forwarding a path with the wrong prefix removed.
610
+
611
+ Plain parameters, regex parameters, and groups within one segment (`/:lang/old/**`, `/x/:id(\d+)/old/**`, `/blog{-:title}?/old/**`) each match exactly one segment and work as expected. A `*` is a catch-all, so a key like `/x/*/old/**` is rejected when the rules are created (Rou3 allows only one catch-all per route). Use `:param` or `([^\x2f]*)` for one segment instead.
612
+
613
+ ### CORS Credentials
614
+
615
+ `credentials: true` requires an explicit `origin` (allowlist or validation function). Combining it with a wildcard origin throws at startup, since `Access-Control-Allow-Origin: *` is invalid for credentialed requests.
616
+
617
+ ## Build-Time Compiler
618
+
619
+ Framework and build-tool authors can compile a rule set into `findRouteRules`. This keeps Rou3 out of the runtime bundle:
620
+
621
+ ```ts
622
+ import { compileRouteRules } from "h3/rules/compiler";
623
+
624
+ const mod = compileRouteRules(config, {
625
+ preMerge: true, // optional: bake pre-merged chains into the generated matcher
626
+ });
627
+
628
+ mod.code; // whole module (also `String(mod)` / template interpolation)
629
+ // -> import { headers as __ruleHandlers__$headers } from "h3/rules";
630
+ // -> export const findRouteRules = (method, path) => ...;
631
+ ```
632
+
633
+ `compileRouteRules` returns three forms:
634
+
635
+ - `imports`: handler import statements.
636
+ - `body`: the `export const findRouteRules = …` declaration.
637
+ - `code`: the complete module, also returned by `String(mod)`.
638
+ Write `code` as a standalone module, or combine `imports` and `body` with a larger generated module.
639
+
640
+ Compiler entry points normalize their input automatically, so you can pass authored configuration directly. Already-normalized rules also work because normalization is idempotent.
641
+
642
+ At runtime, turn `findRouteRules` into a matcher with `createMatcherFromFind(findRouteRules)`. Wrap that matcher with `memoizeRouteRulesMatcher` to enable [memoization](#memoization). Compiled and runtime matchers return the same results.
643
+
644
+ > [!IMPORTANT]
645
+ > The matcher API takes the method **already uppercased** — `findRouteRules`, `createMatcherFromFind`, and `createRouteRulesMatcher` all compare it as given. The `routeRules()` middleware normalizes for you; a hand-written wrapper must pass `event.req.method.toUpperCase()`, or a lowercase-spelled request will match no method-scoped rule at all and skip its gate.
646
+
647
+ > [!NOTE]
648
+ > Rule options are embedded as JS object literals, so they must survive a JSON round-trip. A function, `Date`, or `RegExp` in a rule option (for example a `cors.origin` validation function) fails compilation with an explicit error instead of silently diverging from the runtime matcher.
649
+
650
+ ### Specificity Guard
651
+
652
+ `createMatcherFromFind` applies a **specificity guard by default**. When one of a path's [alternate readings](#encoded-and-alternate-path-spellings) (canonical, slash-merged, percent-decoded) resolves a rule differently from the served path, the alternate reading may only override with an equal-or-more-specific pattern — so a broad `/**` rule can never downgrade a narrower `/admin/**` one on a crafted `%2f` / `%2e%2e` path.
653
+
654
+ The default guard is dependency-free, so a compiled bundle stays free of Rou3. It decides containment from pattern shape, which is a **conservative approximation** of the exact relation: it allows only containment it can prove, so it never permits an override the exact relation would reject — but it is stricter. Where it cannot decide (a named catch-all such as `**:rest`, a regex or partial param, or a modifier param like `:page?` / `:path*`), it keeps the rule the served path resolved rather than applying the narrower one.
655
+
656
+ The same predicate decides when an alternate reading may [bring back a reset permission](#resets-and-the-restricting-flag): only from a pattern equal to or more specific than every pattern that reset it. Here an approximation that is too strict keeps the rule reset, which is safe, while one that is too lax would undo an exemption. A custom predicate must therefore never claim containment it cannot prove.
657
+
658
+ Use [`matcher: true`](#matcher-export) to have the compiler bake the exact relation into the generated module — recommended whenever rule keys use modifier params — or pass your own predicate as the second argument, or `() => true` to disable the guard. Disabling it also lets every alternate reading bring back a permission that a `false` reset removed.
659
+
660
+ Ordering matched layers by specificity does **not** go through this predicate: it uses a rank computed when the rule set is built, so a compiled matcher without the baked relation still resolves the same rules as the runtime matcher.
661
+
662
+ ### Matcher Export
663
+
664
+ To skip the hand-written wrapper, pass `matcher` so the generated module exports a ready-to-use matcher alongside `findRouteRules`:
665
+
666
+ ```ts
667
+ compileRouteRules(config, { matcher: true });
668
+ // -> export const findRouteRules = …;
669
+ // -> import { createMatcherFromFind } from "h3/rules";
670
+ // -> export const matcher = createMatcherFromFind(findRouteRules, /* baked specificity guard */);
671
+
672
+ // rename the export, or bake in memoization:
673
+ compileRouteRules(config, { matcher: { name: "routeMatcher", memoize: true } });
674
+ // -> import { createMatcherFromFind, memoizeRouteRulesMatcher } from "h3/rules";
675
+ // -> export const routeMatcher = memoizeRouteRulesMatcher(createMatcherFromFind(findRouteRules, …));
676
+ ```
677
+
678
+ `matcher: true` names the export `matcher`; pass a string to rename it, or `{ name?, memoize? }` to also wrap it in `memoizeRouteRulesMatcher` (`memoize: { max }` tunes the cap). `memoizeRouteRulesMatcher` is imported **only** when `memoize` is set, so an un-memoized matcher export still tree-shakes it away. The infra import counts toward `mod.imports`.
679
+
680
+ ### Handler Sources
681
+
682
+ The generated module imports **only the rule handlers the rule set uses**. Most built-ins are a named export of `h3/rules` (`headers`, `redirect`, `cors`), except the opt-in subpath handlers: `cache` comes from `h3/rules/cache` and `proxy` from `h3/rules/proxy`, so their dependencies only enter the bundle when a matching rule exists.
683
+
684
+ Where each handler is imported from is controlled by `runtimeRules` — a record keyed by rule name whose value is either a module id (the module must export a member named exactly as the rule key) or `{ source, export }` when the export is named something else. It is merged **over** the built-in preset (`DEFAULT_RUNTIME_RULES`), so you only list what you add or change. Handlers sharing a source collapse into one import statement:
685
+
686
+ ```ts
687
+ import { compileRouteRules } from "h3/rules/compiler";
688
+
689
+ compileRouteRules(config, {
690
+ runtimeRules: {
691
+ cache: "#nitro/cache", // repoint the built-in cache at your own module
692
+ isr: { source: "#nitro/rules", export: "handleISR" }, // custom rule + export
693
+ },
694
+ });
695
+ // -> import { handleISR as __ruleHandlers__$isr } from "#nitro/rules";
696
+ // -> import { cache as __ruleHandlers__$cache } from "#nitro/cache";
697
+ // (redirect, headers, … still import from "h3/rules" when used)
698
+ ```