# H3
> H(TTP) server framework built on top of web standards for high performance and composability.
---
# Getting Started
> Get started with H3.
> [!IMPORTANT]
> You are currently reading H3 v2 docs. See [v1.h3.dev](https://v1.h3.dev/) for legacy docs.
## Overview
⚡ H3 (short for H(TTP), pronounced as /eɪtʃθriː/, like h-3) is a lightweight, fast, and composable server framework for modern JavaScript runtimes. It is based on web standard primitives such as [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request), [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response), [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL), and [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers). You can integrate H3 with any compatible runtime or [mount](/guide/api/h3#h3mount) other web-compatible handlers to H3 with almost no added latency.
H3 is designed to be extendable and composable. Instead of providing one big core, you start with a lightweight [H3 instance](/guide/api/h3) and then import built-in, tree-shakable [utilities](/utils) or bring your own for more functionality.
Composable utilities has several advantages:
- The server only includes used code and runs them exactly where is needed.
- Application size can scale better. Usage of utilities is explicit and clean, with less global impact.
- H3 is minimally opinionated and won't limit your choices.
All utilities, share an [H3Event](/guide/api/h3event) context.
:read-more{to="/utils" title="built-in H3 utilities"}
## Quick Start
> [!TIP]
> You try H3 online [on ⚡️ Stackblitz ](https://stackblitz.com/github/h3js/h3/tree/main/playground?file=server.mjs).
Install `h3` as a dependency:
:pm-install{name="h3"}
Create a new file for server entry:
```ts [server.mjs]
import { H3, serve } from "h3";
const app = new H3().get("/", (event) => "⚡️ Tadaa!");
serve(app, { port: 3000 });
```
Then, run the server using your favorite runtime:
::code-group
```bash [node]
node --watch ./server.mjs
```
```bash [deno]
deno run -A --watch ./server.mjs
```
```bash [bun]
bun run --watch server.mjs
```
::
And tadaa! We have a web server running locally.
### What Happened?
Okay, let's now break down our hello world example.
We first created an [H3](/guide/api/h3) app instance using `new H3()`:
```ts
const app = new H3();
```
[H3](/guide/api/h3) is a tiny class capable of [matching routes](/guide/basics/routing), [generating responses](/guide/basics/response) and calling [middleware](/guide/basics/middleware) and [global hooks](/guide/api/h3#global-hooks).
Then we add a route for handling HTTP GET requests to `/` path.
```ts
app.get("/", (event) => {
return { message: "⚡️ Tadaa!" };
});
```
:read-more{title="Routing" to="/guide/basics/routing"}
We simply returned an object. H3 automatically [converts](/guide/basics/response#response-types) values into web responses.
:read-more{title="Sending Response" to="/guide/basics/response"}
Finally, we use `serve` method to start the server listener. Using `serve` method you can easily start an H3 server in various runtimes.
```js
serve(app, { port: 3000 });
```
> [!TIP]
> The `serve` method is powered by [💥 srvx](https://srvx.h3.dev/), a runtime-agnostic universal server listener based on web standards that works seamlessly with [Deno](https://deno.com/), [Node.js](https://nodejs.org/) and [Bun](https://bun.sh/).
We also have [`app.fetch`](/guide/api/h3#h3fetch) which can be directly used to run H3 apps in any web-compatible runtime or even directly called for testing purposes.
:read-more{to="/guide/api/h3#h3fetch" title="H3.fetch"}
```js
import { H3, serve } from "h3";
const app = new H3().get("/", () => "⚡️ Tadaa!");
// Test without listening
const response = await app.request("/");
console.log(await response.text());
```
You can directly import `h3` library from CDN alternatively. This method can be used for Bun, Deno and other runtimes such as Cloudflare Workers.
```js
import { H3 } from "https://esm.sh/h3";
const app = new H3().get("/", () => "⚡️ Tadaa!");
export const fetch = app.fetch;
```
---
# Request Lifecycle
> H3 dispatches incoming web requests to final web responses.
Below is an overview of what happens in a H3 server from when an HTTP request arrives until a response is generated.
## 1. Incoming Request
When An HTTP request is made by Browser or [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), server fetch handler receives a [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object.
```mermaid
%%{init: {'theme':'neutral'}}%%
flowchart LR
A1["fetch(request)"] --> A2["server.fetch(request)"]
click A2 "/guide/api/h3#h3fetch"
```
> [!TIP]
> [💥 Srvx](https://srvx.h3.dev) provides unified `server.fetch` interface and adds [Node.js compatibility](https://srvx.h3.dev/guide/node).
## 2. Accept Request
H3 Initializes an [`H3Event`](/guide/api/h3event) instance from incoming request, calls [`onRequest`](/guide/api/h3#global-hooks) global hook and finally [`H3.handler`](/guide/api/h3#h3handler) with the initialized event.
```mermaid
%%{init: {'theme':'neutral'}}%%
flowchart LR
B1["new H3Event(request)"] --> B2["onRequest(event)"] --> B3["h3.handler(event)"]
click B1 "/guide/api/h3event"
click B2 "/guide/api/h3#global-hooks"
click B3 "/guide/api/h3#apphandler"
```
## 3. Dispatch Request
H3 [matches route](/guide/basics/routing) based on `request.url` and `request.method`, calls global [middleware](/guide/basics/middleware) and finally matched route handler function with event.
```mermaid
%%{init: {'theme':'neutral'}}%%
sequenceDiagram
participant MiddlewareA as Middleware1(event, next)
participant MiddlewareB as Middleware2(event, next)
participant Route as RouteHandler(event)
MiddlewareA->>+MiddlewareB: await next()
MiddlewareB->>+Route: await next()
Route-->>-MiddlewareB: rawBody
MiddlewareB-->>-MiddlewareA: rawBody
```
> [!TIP]
> 🚀 Internally, H3 uses srvx `FastURL` instead of `new URL(req.url).pathname`.
## 4. Send Response
H3 [converts](/guide/basics/response#response-types) returned value and [prepared headers](/guide/basics/response#preparing-response) into a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response), calls [`onResponse`](/guide/api/h3#global-hooks) global hook and finally returns response back to the server fetch handler.
```mermaid
%%{init: {'theme':'neutral'}}%%
flowchart LR
D1["Returned Value => Response"] --> D2["onResponse(response)"] --> D3["Response"]
click D1 "/guide/basics/response"
click D2 "/guide/api/h3#global-hooks"
```
---
# Routing
> Each request is matched to one (most specific) route handler.
## Adding Routes
You can register route [handlers](/guide/basics/handler) to [H3 instance](/guide/api/h3) using [`H3.on`](/guide/api/h3#h3on), [`H3.[method]`](/guide/api/h3#h3method), or [`H3.all`](/guide/api/h3#h3all).
> [!TIP]
> Router is powered by [🌳 Rou3](https://github.com/h3js/rou3), an ultra-fast and tiny route matcher engine.
**Example:** Register a route to match requests to the `/hello` endpoint with HTTP **GET** method.
- Using [`H3.[method]`](/guide/api/h3#h3method)
```js
app.get("/hello", () => "Hello world!");
```
- Using [`H3.on`](/guide/api/h3#h3on)
```js
app.on("GET", "/hello", () => "Hello world!");
```
You can register multiple event handlers for the same route with different methods:
```js
app
.get("/hello", () => "GET Hello world!")
.post("/hello", () => "POST Hello world!")
.all("/hello", () => "Any other method!");
```
You can also use [`H3.all`](/guide/api/h3#h3all) method to register a route accepting any HTTP method:
```js
app.all("/hello", (event) => `This is a ${event.req.method} request!`);
```
## HEAD Requests
Following [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110#name-head), `HEAD` requests automatically match the corresponding `GET` route and run its handler, but the response body is omitted (only the headers and status are sent). You don't need to register a separate `HEAD` handler:
```js
app.get("/hello", () => "Hello world!");
// HEAD /hello → 200 with the same headers as GET, but an empty body
```
Register an explicit `HEAD` handler when you want to override this — for example, to skip computing the body:
```js
app.head("/hello", (event) => {
event.res.headers.set("content-length", "12");
return null;
});
```
An explicit `head()` route always takes precedence over the automatic `GET` fallback.
## HTTP `QUERY` Method
H3 supports the [HTTP `QUERY` method (RFC 10008)](https://www.rfc-editor.org/rfc/rfc10008) as a first-class method. `QUERY` is like `GET` — **safe, idempotent, and cacheable** — but carries a request body (with a `Content-Type`), closing the long-standing "GET with a body" gap. It's ideal for complex read operations where filters don't fit in a URL.
Register a `QUERY` handler with `app.query()` (or `app.on("QUERY", …)`) and read the request body as usual:
```js
import { readBody } from "h3";
app.query("/search", async (event) => {
const criteria = await readBody(event); // read the query body
return runSearch(criteria);
});
```
Because `QUERY` carries an attacker-controllable body, [body-size limits](/utils/request#assertbodysizeevent-limit) apply just like `POST`.
Two utilities help implement the RFC:
- [`requireContentType(event, acceptedTypes)`](/utils/request#requirecontenttypeevent-acceptedtypes) — assert the request `Content-Type` (`400`/`415`/`422`).
- [`appendAcceptQuery(event, mediaTypes)`](/utils/request#appendacceptqueryevent-mediatypes) — advertise accepted query formats via the `Accept-Query` response header.
> [!NOTE]
> `QUERY` is treated like `GET` for [conditional caching](/utils/request#handlecacheheadersevent-opts) (`304` responses via `handleCacheHeaders`), and [`proxy`](/utils/proxy) forwards it **with** its body. Unlike `GET`, `QUERY` is **not** CORS-safelisted, so browsers send a preflight — if you pass an explicit `methods` allowlist to [`handleCors`](/utils/security#handlecorsevent-options), include `"QUERY"`.
::read-more{to="/examples/handle-query"}
See the [HTTP `QUERY` method example](/examples/handle-query) for a runnable `/books` resource that validates the `Content-Type` and advertises a cacheable `GET` alternative.
::
## Route Patterns
A route pattern is a **pathname**, not a URL: the same shape as `event.url.pathname`, plus [rou3](https://github.com/h3js/rou3) syntax. [`H3.on`](/guide/api/h3#h3on), [`H3.[method]`](/guide/api/h3#h3method), [`H3.all`](/guide/api/h3#h3all), [`H3.use(route, ...)`](/guide/api/h3#h3use), [`H3.mount`](/guide/api/h3#h3mount) and [`removeRoute`](/utils/more#removerouteapp-method-route) normalize it identically, so a middleware registered with the same string as a route always guards that route.
Normalization rules:
- A leading `/` is added when missing (`"hello"` → `/hello`).
- A URL is **rejected** (`app.get("http://example.com/admin")` throws). An authority is never silently dropped: `//admin` registers as the two-segment path `//admin`, not as `/`.
- `.` and `..` segments resolve exactly as the URL parser resolves them in a request path (`/admin/../admin` → `/admin`).
- Characters that a request pathname always carries percent-encoded are encoded: space, non-ASCII, control characters, `"`, `#`, `<`, `>` and `` ` ``. So `app.get("/café")` registers `/caf%C3%A9` — what the browser actually sends.
- Needless escapes are decoded to the literal that the request pathname is canonicalized to (`/%40handle` → `/@handle`, see [Security utils](/utils/security)).
Characters that carry rou3 meaning are left exactly as written, including the escape `\` (never valid in a request pathname). To match `?`, `{`, `}` or `^` **literally**, write it percent-encoded:
```js
app.get("/u/:id?", () => "optional param"); // rou3 syntax, kept as written
app.get("/x%3Fy", () => "literal ?"); // matches the path a client sends for /x?y
```
> [!NOTE]
> Non-ASCII text mixes freely with dynamic syntax — `app.get("/café/:id")` registers `/caf%C3%A9/:id` and matches `/café/42` — with two exceptions, both from the encoded form reaching rou3's own syntax. A **param name** must be ASCII (`[\w-]`): `/:naïve` becomes `/:na%C3%AFve`, which rou3 reads as a param named `na` followed by the literal `%C3%AFve`. And inside a `(...)` group, only literal text and alternation survive encoding (`(café|thé)` works); a **character class** does not, since `[é]` becomes `[%C3%A9]` — write the encoded alternation `(?:%C3%A9)` instead.
## Dynamic Routes
You can define dynamic route parameters using `:` prefix:
```js
// [GET] /hello/Bob => "Hello, Bob!"
app.get("/hello/:name", (event) => {
return `Hello, ${event.context.params.name}!`;
});
```
Instead of named parameters, you can use `*` for unnamed **optional** parameters:
```js
app.get("/hello/*", (event) => `Hello!`);
```
## Wildcard Routes
Adding `/hello/:name` route will match `/hello/world` or `/hello/123`. But it will not match `/hello/foo/bar`.
When you need to match multiple levels of sub routes, you can use `**` prefix:
```js
app.get("/hello/**", (event) => `Hello ${event.context.params._}!`);
```
This will match `/hello`, `/hello/world`, `/hello/123`, `/hello/world/123`, etc.
> [!NOTE]
> Param `_` will store the full wildcard content as a single string.
## Route Meta
You can define optional route meta when registering them, accessible from any middleware.
```js
import { H3 } from "h3";
const app = new H3();
app.use((event) => {
console.log(event.context.matchedRoute?.meta); // { auth: true }
});
app.get("/", (event) => "Hi!", { meta: { auth: true } });
```
::read-more{to="/guide/basics/handler#meta"}
It is also possible to add route meta when defining them using `defineHandler` object syntax.
::
---
# Middleware
> Intercept request, response and errors using H3 middleware.
> [!IMPORTANT]
> We recommend using composable utilities whenever possible. Global middleware can complicate application logic, making it less predictable and harder to understand.
Global middleware run on each request before route handler and act as wrappers to intercept request, response and errors.
:read-more{to="/guide/basics/lifecycle#\_3-dispatch-request" title="Request Lifecycle"}
You can register global middleware to [app instance](/guide/api/h3) using the [`H3.use`](/guide/api/h3#h3use).
**Example:** Register a global middleware that logs every request.
```js
app.use((event) => {
console.log(event);
});
```
**Example:** Register a global middleware that matches certain requests.
```js
app.use(
"/blog/**",
(event, next) => {
console.log("[alert] POST request on /blog paths!");
},
{
method: "POST",
// match: (event) => event.req.method === "POST",
},
);
```
You can register middleware with `next` argument to intercept return values of next middleware and handler.
```js
app.use(async (event, next) => {
const rawBody = await next();
// [intercept response]
return rawBody;
});
```
Example below, always responds with `Middleware 1`.
```js
app
.use(() => "Middleware 1")
.use(() => "Middleware 2")
.get("/", "Hello");
```
> [!IMPORTANT]
> If middleware returns a value other than `undefined` or the result of `next()`, it immediately intercepts request handling and sends a response.
When adding routes, you can register middleware that only run with them.
```js
import { basicAuth } from "h3";
app.get(
"/secret",
(event) => {
/* ... */
},
{
middleware: [basicAuth({ password: "test" })],
},
);
```
For convenience, H3 provides middleware factory functions `onRequest`, `onResponse`, and `onError`:
```js
import { onRequest, onResponse, onError } from "h3";
app.use(
onRequest((event) => {
console.log(`[${event.req.method}] ${event.url.pathname}`);
}),
);
app.use(
onResponse((response, event) => {
console.log(`[${event.req.method}] ${event.url.pathname} ~>`, response.status);
}),
);
app.use(
onError((error, event) => {
console.log(`[${event.req.method}] ${event.url.pathname} !! ${error.message}`);
}),
);
```
---
# Event Handlers
> An event handler is a function that receives an H3Event and returns a response.
You can define typed event handlers using `defineHandler`.
```js
import { H3, defineHandler } from "h3";
const app = new H3();
const handler = defineHandler((event) => "Response");
app.get("/", handler);
```
> [!NOTE]
> Using `defineHandler` is optional.
> You can instead, simply use a function that accepts an [`H3Event`](/guide/api/h3event) and returns a response.
The callback function can be sync or async:
```js
defineHandler(async (event) => "Response");
```
## Object Syntax
### middleware
You can optionally register some [middleware](/guide/basics/middleware) to run with event handler to intercept request, response or errors.
```js
import { basicAuth } from "h3";
defineHandler({
middleware: [basicAuth({ password: "test" })],
handler: (event) => "Hi!",
});
```
:read-more{to="/guide/basics/response" title="Response Handling"}
:read-more{to="/guide/api/h3event" }
### meta
You can define optional route meta attached to handlers, and access them from any other middleware.
```js
import { H3, defineHandler } from "h3";
const app = new H3();
app.use((event) => {
console.log(event.context.matchedRoute?.meta); // { tag: "admin" }
});
app.get("/admin/**", defineHandler({
meta: { tag: "admin" },
handler: (event) => "Hi!",
})
```
::read-more{to="/guide/basics/routing#route-meta"}
It is also possible to add route meta when registering them to app instance.
::
## Handler `.fetch`
Event handlers defined with `defineHandler`, can act as a web handler without even using [H3](/guide/api/h3) class.
```js
const handler = defineHandler(async (event) => `Request: ${event.req.url}`);
const response = await handler.fetch("http://localhost/");
console.log(response, await response.text());
```
## Lazy Handlers
You can define lazy event handlers using `defineLazyEventHandler`. This allow you to define some one-time logic that will be executed only once when the first request matching the route is received.
A lazy event handler must return an event handler.
```js
import { defineLazyEventHandler } from "h3";
defineLazyEventHandler(async () => {
await initSomething(); // Will be executed only once
return (event) => {
return "Response";
};
});
```
This is useful to define some one-time logic such as configuration, class initialization, heavy computation, etc.
Another use-case is lazy loading route chunks:
```js [app.mjs]
import { H3, defineLazyEventHandler } from "h3";
const app = new H3();
app.all(
"/route",
defineLazyEventHandler(() => import("./route.mjs").then((mod) => mod.default)),
);
```
```js [route.mjs]
import { defineHandler } from "h3";
export default defineHandler((event) => "Hello!");
```
## Converting to Handler
There are situations that you might want to convert an event handler or utility made for Node.js or another framework to H3.
There are built-in utils to do this.
### From Web Handlers
Request handlers with [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) => [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) signuture can be converted into H3 event handlers using `fromWebHandler` utility or [H3.mount](/guide/api/h3#h3mount).
```js
import { H3, fromWebHandler } from "h3";
export const app = new H3();
const webHandler = (request) => new Response("👋 Hello!");
// Using fromWebHandler utility
app.all("/web", fromWebHandler(webHandler));
// Using simple wrapper
app.all("/web", (event) => webHandler(event.req));
// Using app.mount
app.mount("/web", webHandler);
```
### From Node.js Handlers
If you have a legacy request handler with `(req, res) => {}` syntax made for Node.js, you can use `fromNodeHandler` to convert it to an h3 event handler.
> [!IMPORTANT]
> Node.js event handlers can only run within Node.js server runtime!
```js
import { H3, fromNodeHandler } from "h3";
// Force using Node.js compatibility (also works with Bun and Deno)
import { serve } from "h3/node";
export const app = new H3();
const nodeHandler = (req, res) => {
res.end("Node handlers work!");
};
app.get("/web", fromNodeHandler(nodeHandler));
```
---
# Sending Response
> H3 automatically converts any returned value into a web response.
Values returned from [Event Handlers](/guide/basics/handler) are automatically converted to a web [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) by H3.
**Example:** Simple event handler function.
```js
const handler = defineHandler((event) => ({ hello: "world" }));
```
H3 smartly converts handler into:
```js
const handler = (event) =>
new Response(JSON.stringify({ hello: "world" }), {
headers: {
"content-type": "application/json;charset=UTF-8",
},
});
```
> [!TIP]
> 🚀 H3 uses srvx `FastResponse` internally to optimize performances in Node.js runtime.
If the returned value from event handler is a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) or from an [async function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function), H3 will wait for it to resolve before sending the response.
If an error is thrown, H3 automatically handles it with error handler.
:read-more{to="/guide/basics/error" title="Error Handling"}
## Preparing Response
Before returning a response in main handler, you can prepare response headers and status using [`event.res`](/guide/api/h3event#eventres).
```js
defineHandler((event) => {
event.res.status = 200;
event.res.statusText = "OK";
event.res.headers.set("Content-Type", "text/html");
return "
Hello, World
";
});
```
> [!NOTE]
> If a full [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response) value is returned, prepared status is discarded and headers will be merged/overridden. For performance reasons, it is best to only set headers only from final Response in this case.
> [!NOTE]
> If an Error happens, prepared status and headers will be discarded. The recommended way to include headers in error responses is via `new HTTPError({ headers })`. As a last resort for headers that need to be set implicitly before the error is known (e.g., CORS), you can use `event.res.errHeaders` — these will be merged into error responses automatically.
## Response Types
H3 smartly converts JavaScript values into web [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response).
### JSON Serializable Value
Returning a [JSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON) serializable value (**object**, **array**, **number** or **boolean**), it will be stringified using [JSON.stringify()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) and sent with default `application/json` content-type.
**Example:**
```ts
app.get("/", (event) => ({ hello: "world" }));
```
> [!TIP]
> Returned objects with `.toJSON()` property can customize serialization behavior. Check [MDN docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) for more info.
### String
Returning a string value, sends it as plain text body.
> [!NOTE]
> If not setting `content-type` header, it can default to `text/plain;charset=UTF-8`.
**Example:** Send HTML response.
```ts
app.get("/", (event) => {
event.res.headers.set("Content-Type", "text/html;charset=UTF-8");
return "hello world
";
});
```
You can also use `html` utility as shortcut. Interpolated values in the tagged template are automatically HTML-escaped to help prevent XSS.
```js
import { html, raw } from "h3";
// Tagged template: interpolations are escaped
app.get("/hello/:name", (event) => html`hello ${event.context.params.name}
`);
// Trusted markup: sent as-is
app.get("/", () => html(raw("hello world
")));
```
> [!IMPORTANT]
> Calling `html()` with a plain string escapes the whole string (a warning is logged if escaping changed it). Use the tagged template for dynamic values, or wrap trusted markup with `raw()`.
### `Response`
Returning a web [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response), sends-it as final response.
**Example:**
```ts
app.get("/", (event) => new Response("Hello, world!", { headers: { "x-powered-by": "H3" } }));
```
> [!IMPORTANT]
> When sending a `Response`, any [prepared headers](#preparing-response) that set before, will be merged as default headers. `event.res.{status,statusText}` will be ignored. For performance reasons, it is best to only set headers only from final `Response`.
>
> If the returned `Response` has an error status (`>= 400`), prepared headers are discarded — same as for a thrown error — and only `event.res.errHeaders` are merged. Success and redirect responses (`< 400`) receive all prepared headers, so `setCookie()` before returning a `302` works as expected.
### `ReadableStream` or `Readable`
Returning a [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) or Node.js [`Readable`](https://nodejs.org/api/stream.html#readable-streams) sends it as stream.
### `ArrayBuffer` or `Uint8Array` or `Buffer`
Send binary [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer), [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) or node [Buffer](https://nodejs.org/api/buffer.html#buffer).
`content-length` header will be automatically set.
### `Blob`
Send a [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) as stream.
`Content-type` and `Content-Length` headers will be automatically set.
### `File`
Send a [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) as stream.
`Content-type`, `Content-Length` and `Content-Disposition` headers will be automatically set.
## Special Types
Some less commonly possible values for response types.
### `null` or `undefined`
Sends a response with empty body.
> [!TIP]
> If there is no `return` statement in event handler, it is same as `return undefined`.
### `Error`
Retuning an [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) instance will send it.
> [!IMPORTANT]
> It is better to `throw` errors instead of returning them. This allows proper propagation from any nested utility.
:read-more{to="/guide/basics/error" title="Error Handling"}
### `BigInt`
Value will be sent as stringified version of BigInt number.
> [!NOTE]
> Returning a JSON object, does not allows BigInt serialization. You need to implement `.toJSON`. Check [MDN docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) for more info.
### `Symbol` or `Function`
**Returning Symbol or Function has undetermined behavior.** Currently, H3 sends a string-like representation of unknown Symbols and Functions but this behavior might be changed to throw an error in the future versions.
There are some internal known Symbols H3 internally uses:
- `Symbol.for("h3.notFound")`: Indicate no route is found to throw a 404 error.
- `Symbol.for("h3.handled")`: Indicate request is somehow handled and H3 should not continue (Node.js specific).
---
# Error Handling
> Send errors by throwing an HTTPError.
H3 captures all possible errors during [request lifecycle](/guide/basics/lifecycle).
## `HTTPError`
You can create and throw HTTP errors using `HTTPError` with different syntaxes.
```js
import { HTTPError } from "h3";
app.get("/error", (event) => {
// Using message and details
throw new HTTPError("Invalid user input", { status: 400 });
// Using HTTPError.status(code)
throw HTTPError.status(400, "Bad Request");
// Using single object
throw new HTTPError({
status: 400,
statusText: "Bad Request",
message: "Invalid user input",
data: { field: "email" },
body: { date: new Date().toJSON() },
headers: {},
});
});
```
This will end the request with `400 - Bad Request` status code and the following JSON response:
```json
{
"date": "2025-06-05T04:20:00.0Z",
"status": 400,
"statusText": "Bad Request",
"message": "Invalid user input",
"data": {
"field": "email"
}
}
```
### `HTTPError` Fields
- `status`: HTTP status code in the range 200–599.
- `statusText`: HTTP status text to be sent in the response header.
- `message`: Error message to be included in the JSON body.
- `data`: Additional data to be attached under the `data` key in the error JSON body.
- `body`: Additional top-level properties to be attached in the error JSON body.
- `headers`: Additional HTTP headers to be sent in the error response.
- `cause`: The original error object that caused this error, useful for tracing and debugging.
- `unhandled`: Indicates whether the error was thrown for unknown reasons. See [Unhandled Errors](#unhandled-errors).
> [!TIP]
> The recommended way to include headers in error responses is to use `new HTTPError({ headers })`:
>
> ```js
> throw new HTTPError({
> status: 400,
> message: "Invalid input",
> headers: { "x-request-id": requestId },
> });
> ```
>
> When an error is thrown, any [prepared headers](/guide/basics/response#preparing-response) set via `event.res.headers` are **not** included in the error response. As a last resort for headers that need to be set implicitly before the error is known (e.g., CORS headers), you can use `event.res.errHeaders`. Built-in utilities like `handleCors` automatically set both.
> [!IMPORTANT]
> Error `statusText` should be short (max 512 to 1024 characters) and only include tab, spaces or visible ASCII characters and extended characters (byte value 128–255). Prefer `message` in JSON body for extended message.
## Unhandled Errors
Any error that occurs during calling [request lifecycle](/guide/basics/lifecycle) without using `HTTPError` will be processed as an _unhandled_ error.
```js
app.get("/error", (event) => {
// This will cause an unhandled error.
throw new Error("Something went wrong");
});
```
> [!TIP]
> For enhanced security, H3 hides certain fields of unhandled errors (`data`, `body`, `stack` and `message`) in JSON response.
## Catching Errors
Using global [`onError`](/guide/api/h3#global-hooks) hook:
```js
import { H3, onError } from "h3";
// Globally handling errors
const app = new H3({
onError: (error) => {
console.error(error);
},
});
```
Using [`onError` middleware](/guide/basics/middleware) to catch errors.
```js
import { onError } from "h3";
// Handling errors using middleware
app.use(
onError((error, event) => {
console.error(error);
}),
);
```
> [!TIP]
> When using nested apps, global hooks of sub-apps will not be called. Therefore it is better to use `onError` middleware.
---
# Nested Apps
> H3 has a native `mount` method for adding nested sub-apps to the main instance.
Typically, H3 projects consist of several [Event Handlers](/guide/basics/handler) defined in one or multiple files (or even [lazy loaded](/guide/basics/handler#lazy-handlers) for faster startup times).
It is sometimes more convenient to combine multiple `H3` instances or even use another HTTP framework used by a different team and mount it to the main app instance. H3 provides a native [`.mount`](/guide/api/h3#h3mount) method to facilitate this.
## Nested H3 Apps
H3 natively allows mounting sub-apps. When mounted, sub-app routes and middleware are **merged** with the base url prefix into the main app instance.
```js
import { H3, serve } from "h3";
const nestedApp = new H3()
.use((event) => {
event.res.headers.set("x-api", "1");
})
.get("/**:slug", (event) => ({
pathname: event.url.pathname,
slug: event.context.params?.slug,
}));
const app = new H3().mount("/api", nestedApp);
```
In the example above, when fetching the `/api/test` URL, `pathname` will be `/api/test` (the real path), and `slug` will be `/test` (wildcard param).
> [!NOTE]
> Global config and hooks won't be inherited from the nested app. Consider always setting them from the main app.
## Nested Web Standard Apps
Mount a `.fetch` compatible server instance like [Hono](https://hono.dev/) or [Elysia](https://elysiajs.com/) under the base URL.
> [!NOTE]
> Base prefix will be removed from `request.url` passed to the mounted app.
```js
import { H3 } from "h3";
import { Hono } from "hono";
import { Elysia } from "elysia";
const app = new H3()
.mount(
"/elysia",
new Elysia().get("/test", () => "Hello Elysia!"),
)
.mount(
"/hono",
new Hono().get("/test", (c) => c.text("Hello Hono!")),
);
```
> [!TIP]
> Similarly, you can mount an H3 app in [Hono](https://hono.dev/docs/api/hono#mount) or [Elysia](https://elysiajs.com/patterns/mount#mount-1).
---
# Route Rules
> Add headers, redirects, CORS, caching, and proxying to groups of routes with one configuration object.
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:
```ts
routeRules({
"/old/**": { redirect: "/new/**" },
"/assets/**": { headers: { "cache-control": "s-maxage=31536000" } },
"/api/**": { cors: true },
});
```
Add `routeRules()` as global middleware. For each request, it:
1. Finds every matching pattern.
2. Merges the matching rules.
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.
4. Makes the merged rules available as `event.context.routeRules`.
> [!TIP]
> 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).
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`.
## Quick Start
```ts [server.mjs]
import { H3, serve } from "h3";
import { routeRules } from "h3/rules";
import { cache } from "h3/rules/cache"; // Needed for `cache` and `swr` (requires ocache)
import { proxy } from "h3/rules/proxy"; // Needed for `proxy`
const app = new H3();
app.use(
routeRules(
{
"/blog/**": { swr: 60 },
"/old/**": { redirect: { to: "/new/**", status: 301 } },
"/api/proxy/**": { proxy: "https://example.com/**" },
"/assets/**": { headers: { "cache-control": "s-maxage=31536000" } },
"/api/**": { cors: true },
"GET /api/cached/**": { swr: 60 }, // applies to GET only
},
{ handlers: { cache, proxy } },
),
);
serve(app);
```
:read-more{to="/guide/basics/middleware" title="Middleware"}
## Built-in Rules
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`.
| Rule | What it does |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `headers` | Set response headers. |
| `redirect` | Send a server-side redirect. |
| `cors` | Handle CORS with [`handleCors`](/utils/security#handlecorsevent-options). Preflight (`OPTIONS`) requests are answered directly. |
| `cache` | Cache the matched route handler's response. Opt-in, see [Caching](#caching). |
| `swr` | Shortcut for `cache: { swr: true, maxAge?: number }`. |
| `proxy` | Forward the request to another origin or an in-app path. Opt-in, see [Proxying](#proxying). |
> [!NOTE]
> 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.
### `headers`
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.
```ts
routeRules({
"/assets/**": { headers: { "cache-control": "s-maxage=31536000" } },
});
```
### `redirect`
Pass a string to use the default `307` status. Pass `{ to, status }` to choose the status:
```ts
routeRules({
"/old/**": { redirect: "/new/**" }, // /old/a?x=1 → /new/a?x=1 (307)
"/moved/**": { redirect: "/new?from=**" }, // /moved/a/b → /new?from=a/b
"/search": { redirect: "https://example.com/s?lang=en" }, // /search?q=h3 → …/s?lang=en&q=h3
"/legacy": { redirect: { to: "/", status: 301 } },
});
```
Redirects and [proxies](#proxying) share two useful behaviors:
- **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.
- **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.
### `cors`
Pass `true` to use permissive defaults. Pass a `CorsOptions` object to configure an origin allowlist, credentials, `maxAge`, and other options:
```ts
routeRules({
"/api/**": { cors: { origin: ["https://example.com"], credentials: true } },
});
```
> [!NOTE]
> 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.
### `cache` and `swr`
Caches the response from the matched route handler. You must register a cache handler first. See [Caching](#caching).
`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.
### `proxy`
Forward matching requests elsewhere. This rule needs the opt-in handler from `h3/rules/proxy` — see [Proxying](#proxying).
## How Matching Works
Route rules use [🌳 Rou3](https://github.com/h3js/rou3), the same engine as [routing](/guide/basics/routing). Patterns are matched against `event.url.pathname`.
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.
```ts
routeRules({
"/**": { headers: { "x-app": "demo" } },
"/api/**": { headers: { "x-api": "1" } },
});
// GET /api/users → x-app: demo, x-api: 1
```
### Resetting a Rule
Set a rule to `false` on a more specific pattern to turn off inherited behavior for that part of your app:
```ts
routeRules({
"/api/**": { cors: { origin: ["https://example.com"] } },
"/api/public/**": { cors: false }, // no CORS handling under /api/public
});
```
### Method-Scoped Rules
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.
```ts
routeRules({
"/api/**": { headers: { "x-api": "1" } }, // any method
"GET /api/**": { swr: 60 }, // GET only
});
```
H3 groups equivalent route patterns together. For example, `/users/*`, `/users/:id`, and `/users/:userId` describe the same route group. A general rule using one spelling can therefore merge with a method-specific rule using another spelling.
> [!NOTE]
> If equivalent spellings have different specificity, such as `/a/*` and `/a/:id`, the more general pattern resolves last and wins. This applies whether or not the rules are method-scoped. To avoid surprising results, use one spelling consistently.
### Reading Matched Rules
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:
```ts
app.get("/blog/:slug", (event) => {
const rules = event.context.routeRules;
rules?.cache; // { swr: true, maxAge: 60 }
rules?.redirect?.to; // "/new"
rules?.headers?.["x-a"]; // "1"
});
```
> [!IMPORTANT]
> 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**.
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:
```ts
const { routeRules, matchedRules } = matcher("GET", "/blog/post");
routeRules.cache; // { swr: true, maxAge: 60 } — same object the context gets
matchedRules.cache?.route; // "/blog/**"
matchedRules.cache?.params; // rou3 params of the contributing patterns
```
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.
### Data-Only Rules
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.
```ts
routeRules({
"/docs/**": { prerender: true },
});
// event.context.routeRules.prerender → true
```
Declare custom data-only keys to make them type-safe. See [TypeScript](#typescript).
## Caching
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.
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.
```ts
import { routeRules } from "h3/rules";
import { cache } from "h3/rules/cache";
// default: in-memory storage
app.use(routeRules({ "/blog/**": { swr: 60 } }, { handlers: { cache } }));
```
The default handler uses in-memory storage. Create your own handler instance to change the storage or default options:
```ts
import { createOcacheRuleHandler } from "h3/rules/cache";
app.use(
routeRules(rules, {
handlers: {
cache: createOcacheRuleHandler({
storage: myStorage, // ocache storage instance (or a factory), shared by every rule
defaults: { staleMaxAge: 60 }, // ocache defaults incl. hooks (rule options win)
}),
},
}),
);
```
### How Entries Are Keyed
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 `:::`:
- 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.
- The **method** prevents a body-less `HEAD` response from being stored as the `GET` response.
You can override `group` and `name` as normal `cache` rule options. Be careful: an explicit `name` replaces the entire default name, including its isolation.
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.
> [!NOTE]
> 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.
### What Reaches a Cached Handler
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:
- **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.
- **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.
- 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`.
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.
### Credentials and Cookies
> [!IMPORTANT]
> 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.
- 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.
- 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.
- 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.
- 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.
### Cache-Control Behavior
- H3 preserves a handler's `Cache-Control` header when it contains `private` or `no-store`. Ocache also refuses to store the response.
- 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.
- Set `sendCacheControl: false` to disable the generated header.
### Bring Your Own Cache
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:
```ts
import { createCacheRuleHandler } from "h3/rules";
const cache = createCacheRuleHandler({
defineCachedHandler: (handler, opts) => myCachedHandler(handler, opts),
});
```
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.
## Proxying
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:
```ts
import { routeRules } from "h3/rules";
import { proxy } from "h3/rules/proxy";
app.use(
routeRules({ "/api/proxy/**": { proxy: "https://example.com/**" } }, { handlers: { proxy } }),
);
```
Proxy targets behave like [`redirect`](#redirect) targets: h3 substitutes matching `/**` tails and forwards the query string.
> [!TIP]
> 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.
## Execution Order
Rules that have runtime handlers run as middleware. Lower order numbers run first and wrap the rules inside them:
```
cors (-3) → [-2 free] → headers (-1) → custom rules (0) → redirect (1) → proxy (2) → cache (3) → route handler
```
In practice:
- CORS can answer a preflight request before any other rule runs.
- `headers` wraps every rule inside it, so its values override headers produced by caching or other inner rules.
- `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.
- Because `cache` dispatches the route itself, it also ends the app's _global_ middleware chain for every request it can cache. See [Cached Routes and Global Middleware](#cached-routes-and-global-middleware).
- Custom rules use order `0` by default, so they run before `redirect`, `proxy`, and `cache`.
`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.
## Options
Pass these options as the second argument to `routeRules(config, options)`:
| Option | Description |
| ---------- | --------------------------------------------------------------------------------------------------- |
| `baseURL` | Prefix every rule pattern (trailing slash trimmed). |
| `handlers` | Add or override rule handlers by name. `undefined` makes that rule data-only. |
| `memoize` | Memoize match results per `method + pathname`. Enabled by default, see [Memoization](#memoization). |
| `preMerge` | Resolve each pattern's subsumption chain at startup, see [Pre-merging](#pre-merging). |
## Custom Rule Handlers
Use a custom handler when you need runtime behavior that is not built in. A handler definition has the shape `{ handler, order? }`:
- `handler` turns a matched rule into H3 [middleware](/guide/basics/middleware).
- `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).
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`.
```ts
app.use(
routeRules(
{ "/x/**": { shout: "hello" } },
{
handlers: {
shout: {
handler: (matched) => (event) => {
event.res.headers.set("x-shout", String(matched.options).toUpperCase());
},
},
},
},
),
);
```
> [!IMPORTANT]
> 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.
## Performance
### Memoization
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.
- The memoization map holds up to `1024` entries by default. Dynamic paths therefore cannot grow it without limit. Change the cap with `memoize: { max }`.
- 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.
- Use `memoize: false` to resolve every request again and create fresh result objects.
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.
### Pre-merging
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.
Pre-merging only works for rule sets whose relationships can be determined in advance. It cannot safely analyze partial overlaps such as `/a/*/c` and `/a/b/*`, where the most specific match is ambiguous, or patterns with regex parameters:
- The **runtime matcher throws during startup**.
- The **compiler falls back safely**. It logs a warning and uses normal compilation, so the generated matcher remains correct.
## Using Matchers Directly
Most apps should use `routeRules()`. Frameworks that need to resolve rules outside middleware can use the lower-level exports directly:
```ts
import { createRouteRulesMatcher, normalizeRouteRules, memoizeRouteRulesMatcher } from "h3/rules";
import { cache } from "h3/rules/cache";
const matcher = memoizeRouteRulesMatcher(
createRouteRulesMatcher(normalizeRouteRules(config), {
baseURL: "/base",
preMerge: true,
handlers: { cache },
}),
);
const { routeRules, routeRuleMiddleware } = matcher("GET", "/blog/post");
```
- `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.
- A match returns `{ routeRules, matchedRules, routeRuleMiddleware }`: the merged values placed on the event context, those values with pattern provenance, and the ordered middleware chain.
- `mergeMatchedRouteRules()` is the pure merge operation: it accepts matched layers and returns matched rules. `ruleHandlers` is the default registry for `headers`, `redirect`, and `cors`.
## TypeScript
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:
```ts
declare module "h3/rules" {
interface RouteRuleConfig {
/** Incremental Static Regeneration (handled at build time). */
isr?: number | boolean;
/** Add this route to the prerender queue. */
prerender?: boolean;
/** A data-only rule with no runtime handler. */
audience?: "public" | "internal";
}
interface RouteRules {
isr?: number | boolean;
prerender?: boolean;
audience?: "public" | "internal";
}
}
// event.context.routeRules.audience → "public" | "internal" | undefined
```
Key points:
- `RouteRuleConfig` is **closed**. Unknown keys cause type errors, so TypeScript catches a typo such as `redirct`. Module augmentation adds your custom keys.
- `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.
- Data-only rules pass through normalization and merging unchanged. Module augmentation affects only their types.
- 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`.
### Redeclaring Built-in Rules
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:
```ts
declare module "h3" {
interface RouteRules {
redirect?: string | { to: string; status?: number };
cors?: boolean;
}
}
```
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.
> [!NOTE]
> 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.
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).
## Security
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.
### Encoded and Alternate Path Spellings
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.
[`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.
Route patterns, however, are normally written with the character itself. To prevent encoded paths from bypassing those patterns, h3 also resolves each request against:
- its **canonical** reading (encoded separators decoded, `.` / `..` resolved),
- its **slash-merged** reading (what a downstream like nginx `merge_slashes` resolves),
- its **percent-decoded** reading.
```ts
routeRules({
"/@admin/**": { redirect: "/elsewhere" },
"/a admin/**": { redirect: "/elsewhere" },
});
// GET /@admin/data → matched
// GET /%40admin/data → matched (h3 serves it as /@admin/data)
// GET /a%20admin/data → matched (a proxied backend would serve it as "/a admin/data")
// GET /a%2520admin/x → matched (…and so would one that decodes twice)
// GET /admin%2fpanel → matched against /admin/panel too
```
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.
H3 also normalizes encoded rule patterns at configuration time. For example, `/a%20admin/**` becomes `/a admin/**`, so the pattern does not cover only the encoded spelling.
Rule keys decode escapes in the same way as h3 route patterns. An encoded Rou3 metacharacter therefore becomes a metacharacter: `"/a/%3Aid"` is the `:id` parameter pattern, and `"/f/%2A%2A"` is a catch-all. This matches the behavior of `app.get("/a/%3Aid")`. Only `%2f`, `%5c`, and `%25` remain encoded and match literally, because decoding them would change the number of path segments.
Dispatch is unaffected: routing still uses `event.url.pathname` as served, and `redirect` / `proxy` still forward the raw path bytes.
### Resets and the `restricting` Flag
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:
- A rule that **permits** behavior stays reset. Restoring it could loosen the response, so the exemption wins. For example, a crafted path cannot undo `cors: false` on a private subtree. All built-in rules (`cors`, `redirect`, `headers`, `cache`, and `proxy`) belong to this category.
- 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.
Handlers declare which they are via `RuleHandler.restricting`, which defaults to `false`. No built-in sets it.
> [!IMPORTANT]
> 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. 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.
One residual, in the fail-safe direction: a permission reset is never resurrected, even by a reading that is a faithful re-spelling of the path that granted it. `{"/**": { cors }, "/app/*": { 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 _not_ applying a permission.
### `HEAD` and Preflight Requests
`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.
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.
### Headers on Short-Circuited Responses
`headers` sits at order `-1`, so it wraps every rule that runs inside it — but not one that runs _outside_ 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).
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`.
### Cached Routes and Global Middleware
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 _miss_ just as much as on a hit:
```ts
app.use(routeRules({ "/api/**": { swr: 60 } }, { handlers: { cache } }));
app.use(requireAuth); // never runs for a cacheable /api/** request — not even the first
app.get("/api/private/:id", handler);
```
Register `routeRules()` **after** every global middleware that has to run for cached routes:
```ts
app.use(requireAuth); // runs first, for every request
app.use(routeRules({ "/api/**": { swr: 60 } }, { handlers: { cache } }));
```
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`.
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:
```ts
app.use(routeRules({ "/api/**": { swr: 60 } }, { handlers: { cache } }));
app.use(requireAuth); // skipped for a cacheable GET — but runs for POST, PUT, ...
app.get("/api/non-cachable/:id", handler);
app.post("/api/non-cachable/:id", handler);
```
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.
That same composition is why `routeRules()` belongs in `app.use()`, not on a route:
```ts
// Works, but the whole rule chain runs twice per request:
app.get("/api/x", handler, { middleware: [routeRules(rules, { handlers: { cache } })] });
```
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.
### Redirect and Proxy Target Safety
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`.
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.
Some patterns can match a variable number of prefix segments. These include a catch-all (`/a/**/old/**`), a modifier parameter (`/:lang?/old/**`, `/x/:seg*/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.
Plain parameters, `*`, regex parameters, and groups within one segment (`/:lang/old/**`, `/x/*/old/**`, `/blog{-:title}?/old/**`) each match exactly one segment and work as expected.
### CORS Credentials
`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.
## Build-Time Compiler
Framework and build-tool authors can compile a rule set into `findRouteRules`. This keeps Rou3 out of the runtime bundle:
```ts
import { compileRouteRules } from "h3/rules/compiler";
const mod = compileRouteRules(config, {
preMerge: true, // optional: bake pre-merged chains into the generated matcher
});
mod.code; // whole module (also `String(mod)` / template interpolation)
// -> import { headers as __ruleHandlers__$headers } from "h3/rules";
// -> export const findRouteRules = (method, path) => ...;
```
`compileRouteRules` returns three forms:
- `imports`: handler import statements.
- `body`: the `export const findRouteRules = …` declaration.
- `code`: the complete module, also returned by `String(mod)`.
Write `code` as a standalone module, or combine `imports` and `body` with a larger generated module.
Compiler entry points normalize their input automatically, so you can pass authored configuration directly. Already-normalized rules also work because normalization is idempotent.
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.
> [!IMPORTANT]
> 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.
> [!NOTE]
> 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.
### Specificity Guard
`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.
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.
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.
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.
### Matcher Export
To skip the hand-written wrapper, pass `matcher` so the generated module exports a ready-to-use matcher alongside `findRouteRules`:
```ts
compileRouteRules(config, { matcher: true });
// -> export const findRouteRules = …;
// -> import { createMatcherFromFind } from "h3/rules";
// -> export const matcher = createMatcherFromFind(findRouteRules, /* baked specificity guard */);
// rename the export, or bake in memoization:
compileRouteRules(config, { matcher: { name: "routeMatcher", memoize: true } });
// -> import { createMatcherFromFind, memoizeRouteRulesMatcher } from "h3/rules";
// -> export const routeMatcher = memoizeRouteRulesMatcher(createMatcherFromFind(findRouteRules, …));
```
`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`.
### Handler Sources
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.
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:
```ts
import { compileRouteRules } from "h3/rules/compiler";
compileRouteRules(config, {
runtimeRules: {
cache: "#nitro/cache", // repoint the built-in cache at your own module
isr: { source: "#nitro/rules", export: "handleISR" }, // custom rule + export
},
});
// -> import { handleISR as __ruleHandlers__$isr } from "#nitro/rules";
// -> import { cache as __ruleHandlers__$cache } from "#nitro/cache";
// (redirect, headers, … still import from "h3/rules" when used)
```
---
# H3
> H3 class is the core of server.
You can create a new H3 app instance using `new H3()`:
```js
import { H3 } from "h3";
const app = new H3({/* optional config */});
```
## `H3` Methods
### `H3.request`
A [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)-compatible function allowing to fetch app routes.
- Input can be a relative path, [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL), or [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request).
- Returned value is a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) promise.
```ts
const response = await app.request("/");
console.log(response, await response.text());
```
### `H3.fetch`
Similar to `H3.request` but only accepts one `(req: Request)` argument for cross runtime compatibility.
### `H3.on`
Register route handler for specific HTTP method.
```js
const app = new H3().on("GET", "/", () => "OK");
```
:read-more{to="/guide/basics/routing" title="Routing"}
### `H3.[method]`
Register route handler for specific HTTP method (shortcut for `app.on(method, ...)`).
```js
const app = new H3().get("/", () => "OK");
```
### `H3.all`
Register route handler for all HTTP methods.
```js
const app = new H3().all("/", () => "OK");
```
### `H3.use`
Register a global [middleware](/guide/basics/middleware).
```js
const app = new H3()
.use((event) => {
console.log(`request: ${event.req.url}`);
})
.all("/", () => "OK");
```
:read-more{to="/guide/basics/middleware" title="Middleware"}
### `H3.register`
Register a H3 plugin to extend app.
:read-more{to="/guide/advanced/plugins" title="Plugins"}
### `H3.handler`
An H3 [event handler](/guide/basics/handler) useful to compose multiple H3 app instances.
**Example:** Nested apps.
```js
import { H3, serve, redirect, withBase } from "h3";
const nestedApp = new H3().get("/test", () => "/test (sub app)");
const app = new H3()
.get("/", (event) => redirect(event, "/api/test"))
.all("/api/**", withBase("/api", nestedApp.handler));
serve(app);
```
### `H3.mount`
Using `.mount` method, you can register a sub-app with prefix.
:read-more{to="/guide/basics/nested-apps" title="Nested Apps"}
## `H3` Options
You can pass global app configuration when initializing an app.
Supported options:
- `debug`: Displays debugging stack traces in HTTP responses (potentially dangerous for production!).
- `silent`: When enabled, console errors for unhandled exceptions will not be displayed.
- `allowMalformedURL`: When enabled, requests with a malformed percent-encoded URL path (e.g. `/foo%`, `/%ZZ`) are allowed through with the raw pathname instead of being rejected with a `400 Bad Request` (the default).
- `plugins`: (see [plugins](/guide/advanced/plugins) for more information)
> [!IMPORTANT]
> Enabling `debug` option, sends important stuff like stack traces in error responses. Only enable during development.
### Global Hooks
When initializing an H3 app, you can register global hooks:
- `onError`
- `onRequest`
- `onResponse`
These hooks are called for every request and can be used to add global logic to your app such as logging, error handling, etc.
```js
const app = new H3({
onRequest: (event) => {
console.log("Request:", event.req.url);
},
onResponse: (response, event) => {
console.log("Response:", event.url.pathname, response.status);
},
onError: (error, event) => {
console.error(error);
},
});
```
> [!IMPORTANT]
> Global hooks only run from main H3 app and **not** sub-apps. Use [middleware](/guide/basics/middleware) for more flexibility.
## `H3` Properties
### `H3.config`
Global H3 instance config.
---
# H3Event
> H3Event, carries incoming request, prepared response and context.
With each HTTP request, H3 internally creates an `H3Event` object and passes it though event handlers until sending the response.
:read-more{to="/guide/basics/lifecycle" title="Request Lifecycle"}
An event is passed through all the lifecycle hooks and composable utils to use it as context.
**Example:**
```js
app.get("/", async (event) => {
// Log HTTP request
console.log(`[${event.req.method}] ${event.req.url}`);
// Parsed URL and query params
const searchParams = event.url.searchParams;
// Try to read request JSON body
const jsonBody = await event.req.json().catch(() => {});
return "OK";
});
```
## `H3Event` Methods
### `H3Event.waitUntil`
Tell the runtime about an ongoing operation that shouldn't close until the promise resolves.
```js [app.mjs]
import { logRequest } from "./tracing.mjs";
app.get("/", (event) => {
request.waitUntil(logRequest(request));
return "OK";
});
```
```js [tracing.mjs]
export async function logRequest(request) {
await fetch("https://telemetry.example.com", {
method: "POST",
body: JSON.stringify({
method: request.method,
url: request.url,
ip: request.ip,
}),
});
}
```
> [!TIP]
> To release per-request resources (timers, upstream connections, file handles) once the event is fully over — on every runtime — use the [`onDispose(event, cb)`](/utils/response#ondisposeevent-cb) utility.
## `H3Event` Properties
### `H3Event.app?`
Access to the H3 [application instance](/guide/api/h3).
### `H3Event.context`
The context is an object that contains arbitrary information about the request.
You can store your custom properties inside `event.context` to share across utils.
**Known context keys:**
- `context.params`: Matched router parameters.
- `middlewareParams`: Matched middleware parameters
- `matchedRoute`: Matched router route object.
- `sessions`: Cached session data.
- `basicAuth`: Basic authentication data.
### `H3Event.req`
Incoming HTTP request info based on native [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) with additional runtime addons (see [srvx docs](https://srvx.h3.dev/guide/handler#extended-request-context)).
```ts
app.get("/", async (event) => {
const url = event.req.url;
const method = event.req.method;
const headers = event.req.headers;
// (note: you can consume body only once with either of this)
const bodyStream = await event.req.body;
const textBody = await event.req.text();
const jsonBody = await event.req.json();
const formDataBody = await event.req.formData();
return "OK";
});
```
### `H3Event.url`
Access to the full parsed request [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL).
```ts
app.get("/", (event) => {
const { pathname, search, searchParams } = event.url;
return "OK";
});
```
#### Pathname encoding
`event.url.pathname` is the path in its wire encoding, with one exception: an escape that is _needlessly_ there is dropped.
An escape is needless when every decoding consumer — a proxy, a filesystem lookup, a handler calling `decodeURIComponent` — reads it as its literal, while H3's matchers compare the two as different strings. Left alone, that gap lets `/%61dmin` slip past an `/admin` guard and still reach the `/admin` route, or reach a catch-all handler that decodes it downstream. So H3 decodes exactly those escapes, once, before routing. Nothing else is touched:
| Request | `event.url.pathname` | Why |
| ------------- | -------------------- | ---------------------------------------------- |
| `/%61dmin` | `/admin` | Unreserved escape, needlessly encoded |
| `/a%2eb` | `/a.b` | Unreserved escape, needlessly encoded |
| `/a%21b` | `/a!b` | Decodes downstream, needlessly encoded |
| `/%40handle` | `/@handle` | Decodes downstream, needlessly encoded |
| `/a/%2e%2e/b` | `/b` | The URL parser resolved the dot segment first |
| `/x%2fy` | `/x%2fy` | Separator, must stay encoded |
| `/x%5cy` | `/x%5cy` | Separator, must stay encoded |
| `/100%25` | `/100%25` | Decoding it would expose a nested escape |
| `/a%20b` | `/a%20b` | The URL serializer re-encodes a space anyway |
| `/caf%C3%A9` | `/caf%C3%A9` | The URL serializer re-encodes non-ASCII anyway |
The decoded set is every escape whose character survives WHATWG path serialization unchanged, minus `%2f` and `%25`: the [RFC 3986 §2.3](https://www.rfc-editor.org/rfc/rfc3986#section-2.3) unreserved set (`ALPHA` / `DIGIT` / `-` / `.` / `_` / `~`), which is equivalent to its literal per [§6.2.2.2](https://www.rfc-editor.org/rfc/rfc3986#section-6.2.2.2), plus `!`, `$`, `&`, `'`, `(`, `)`, `*`, `+`, `,`, `:`, `;`, `=`, `@`, `[`, `]` and `|`. Anything else is left alone because decoding it cannot survive the round trip (`%20`, `%5E`, `%7B`, non-ASCII), would change how many segments the path has (`%2f`, `%5c`), would delete the character outright (`%09` and the other C0 controls), or would expose a nested escape (`%25`).
That is wider than [`decodeURI`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURI), which preserves all of RFC 3986's reserved set (`; / ? : @ & = + $ ,`) — of which only `/` is structural in an already-parsed path. Routes like `/@handle` or `/resource:action` are ordinary, so a guard protecting one must not be walkable past by its escaped spelling (`/%40handle`).
Route matching, `use()` matchers and your own `event.url.pathname` checks therefore all compare one and the same string. `event.req.url` always keeps the original wire encoding, so for a non-canonical path the two disagree: read the path from `event.url`, and never re-derive it from `event.req.url` — slicing one by an offset taken from the other is how mount prefixes and proxy targets desync.
Canonicalization happens in the `H3Event` constructor, so it covers every event, including one built by `mockEvent()` or a standalone `handler.fetch()`.
Every escape that survives is **opaque** — treat it that way:
> [!WARNING]
> Never decode `event.url.pathname` yourself. Decoding can reintroduce a `/` or `..` that routing and middleware never saw, which is a path traversal vector when the value reaches a filesystem or an upstream URL. To read a route param in decoded form, use [`getRouterParams(event, { decode: true })`](/utils/request#getrouterparamsevent-opts-decode), which decodes everything else but keeps encoded separators encoded. To canonicalize a path for a scope check, use [`resolveDotSegments`](/utils/security#resolvedotsegments).
A route param therefore can never contain a path separator the router did not match on: `%2f` and `%5c` stay encoded, so `/a%2fb` and `/a%5cb` are one segment (matching the route `/:id`, not `/a/:id`).
Requests with malformed percent-encoding (such as `/foo%` or `/%ZZ`) have no canonical form to decode to and are rejected with a `400 Bad Request` before any handler runs. Set the [`allowMalformedURL`](/guide/api/h3#h3-options) app option to receive the raw pathname instead.
### `H3Event.res`
Prepared HTTP response status and headers.
```ts
app.get("/", (event) => {
event.res.status = 200;
event.res.statusText = "OK";
event.res.headers.set("x-test", "works");
return "OK";
});
```
:read-more{to="/guide/basics/response#preparing-response" title="Preparing Response"}
---
# Plugins
> H3 plugins allow you to extend an H3 app instance with reusable logic.
## Register Plugins
Plugins can be registered either when creating a new [H3 instance](/guide/api/h3) or by using [H3.register](/guide/api/h3#h3register).
```js
import { H3 } from "h3";
import { logger } from "./logger.mjs";
// Using instance config
const app = new H3({
plugins: [logger()],
});
// Or register later
app.register(logger());
// ... rest of the code..
app.get("/**", () => "Hello, World!");
```
> [!NOTE]
> Plugins are always registered immediately. Therefore, the order in which they are used might be important depending on the plugin's functionality.
## Creating Plugins
H3 plugins are simply functions that accept an [H3 instance](/guide/api/h3) as the first argument and immediately apply logic to extend it.
```js
app.register((app) => {
app.use(...)
})
```
For convenience, H3 provides a built-in `definePlugin` utility, which creates a typed factory function with optional plugin-specific options.
```js
import { definePlugin } from "h3";
const logger = definePlugin((h3, _options) => {
if (h3.config.debug) {
h3.use((req) => {
console.log(`[${req.method}] ${req.url}`);
});
}
});
```
---
# WebSockets
> H3 has built-in utilities for cross platform WebSocket and Server-Sent Events.
You can add cross platform WebSocket support to H3 servers using [🔌 CrossWS](https://crossws.h3.dev/).
## Usage
WebSocket handlers can be defined using the `defineWebSocketHandler()` utility and registered to any route like event handlers.
You need to register CrossWS as a server plugin in the `serve` function. The plugin resolves the correct hooks from your matched route automatically.
```js
import { H3, serve, defineWebSocketHandler } from "h3";
import { plugin as ws } from "crossws/server";
const app = new H3();
app.get("/_ws", defineWebSocketHandler({ message: console.log }));
serve(app, {
plugins: [ws()],
});
```
> [!NOTE]
> Passing a custom `resolve` to `ws()` is only needed to resolve hooks yourself (for example without invoking the app). By default, CrossWS calls the app's `fetch` handler and reads the hooks attached by `defineWebSocketHandler()`.
`defineWebSocketHandler()` attaches the hooks to the **request** (CrossWS reads them back with `getWebSocketHooks(request)`, keyed by `Symbol.for("crossws.hooks")`), and answers the upgrade request with `426 Upgrade Required` for anything that is not a WebSocket client.
They are attached to the request rather than to that response because a `Response` gets rebuilt on its way out of an app — merging a header staged by a [route rule](/guide/rules) or CORS middleware, or any middleware doing `new Response(res.body, res)` — and a rebuilt response carries none of the original's own properties. The response also exposes the hooks as `res.crossws` for convenience, but only when nothing rebuilt it; if you write a custom `resolve`, read the request instead:
```js
import { getWebSocketHooks } from "crossws";
serve(app, {
plugins: [ws({ resolve: (req) => app.fetch(req).then(() => getWebSocketHooks(req)) })],
});
```
**Full example:**
```js [websocket.mjs]
import { H3, serve, html, defineWebSocketHandler } from "h3";
import { plugin as ws } from "crossws/server";
export const app = new H3();
// A minimal self-contained WebSocket playground served for plain HTTP requests.
const playground = html`
H3 WebSocket Playground
H3 WebSocket Playground
`;
// A single route serves both the playground page (plain HTTP) and the
// WebSocket endpoint (upgrade requests). The page connects back to itself.
app.get(
"/",
defineWebSocketHandler(
{
open(peer) {
console.log("[open]", peer);
// Send welcome to the new client
peer.send("Welcome to the server!");
// Join new client to the "chat" channel
peer.subscribe("chat");
// Notify every other connected client
peer.publish("chat", `[system] ${peer} joined!`);
},
message(peer, message) {
console.log("[message]", peer);
if (message.text() === "ping") {
// Reply to the client with a ping response
peer.send("pong");
return;
}
// The server re-broadcasts incoming messages to everyone
peer.publish("chat", `[${peer}] ${message}`);
// Echo the message back to the sender
peer.send(message);
},
close(peer) {
console.log("[close]", peer);
peer.publish("chat", `[system] ${peer} has left the chat!`);
peer.unsubscribe("chat");
},
},
// Non-upgrade requests get the playground page.
() => playground,
),
);
serve(app, {
plugins: [ws()],
});
```
### Handling HTTP requests
By default, a WebSocket route responds with `426 Upgrade Required` to any request that is not a WebSocket upgrade.
You can pass an optional HTTP handler as the second argument to `defineWebSocketHandler()` to serve regular (non-upgrade) requests on the same route. WebSocket upgrade requests still go to the hooks.
```js
app.get(
"/_ws",
defineWebSocketHandler(
{ message: (peer, message) => peer.send(message.text()) },
() => "Send a WebSocket upgrade request to connect.",
),
);
```
## Server-Sent Events (SSE)
As an alternative to WebSockets, you can use [Server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events).
H3 has a built-in `EventStream` class to create server-sent events. Construct it directly with `new EventStream(event)` and return it from a handler.
### Example
```js [server-sent-events.mjs]
import { H3, serve, EventStream } from "h3";
export const app = new H3();
app.get("/", (event) => {
const eventStream = new EventStream(event);
// Send a message every second
const interval = setInterval(async () => {
await eventStream.push("Hello world");
}, 1000);
// cleanup the interval when the connection is terminated or the writer is closed
eventStream.onClosed(() => {
console.log("Connection closed");
clearInterval(interval);
});
return eventStream;
});
serve(app);
```
---
# Nightly Builds
You can opt-in to early test latest H3 changes using automated nightly release channel.
If you are directly using `h3` as a dependency in your project:
```json
{
"dependencies": {
"h3": "npm:h3-nightly@latest"
}
}
```
---
# H3 Utils
H3 is a composable framework. Instead of providing a big core, you start with a lightweight [H3](/guide/api/h3) instance and for every functionality, there is either a built-in utility or you can make yours.
::card-group
::card
---
title: Request
icon: material-symbols-light:input
to: /utils/request
---
Utilities for incoming request.
::
::card
---
title: Response
icon: material-symbols-light:output
to: /utils/response
---
Utilities for preparing and sending response.
::
::card
---
title: Cookie
icon: material-symbols:cookie-outline
to: /utils/cookie
---
Cookie utilities.
::
::card
---
title: Security
icon: wpf:key-security
to: /utils/security
---
Security utilities.
::
::card
---
title: Proxy
icon: arcticons:super-proxy
to: /utils/proxy
---
Proxy utilities.
::
::card
---
title: MCP
icon: material-symbols:swap-calls
to: /utils/mcp
---
MCP related utilities.
::
::card
---
title: More
icon: ri:more-line
to: /utils/more
---
More Utilities.
::
::card
---
title: Community
icon: pixelarticons:github
to: /utils/community
---
Community made utilities.
::
::
---
# Request
> H3 request utilities.
## Body
### `assertBodySize(event, limit)`
Asserts that the request body size is within the specified limit.
The limit is enforced **as the body is read**, not by pre-buffering: the request is wrapped by srvx's `limitRequestBody`, which counts bytes as they flow and aborts with a `413` {@link HTTPError} the moment the running total exceeds `limit` (the error is injected via `createError`). This preserves the byte-accurate guarantee (a lying-small `Content-Length` is still caught mid-stream) without holding the body in memory or blocking streaming handlers.
An honest `Content-Length` that already exceeds the limit is rejected up-front with a `413`, and a request carrying both `Content-Length` and `Transfer-Encoding` is rejected with a `400` (request smuggling, RFC 7230).
Because enforcement is tied to consumption, an overflow on a chunked / unknown-length body surfaces when the handler reads the body rather than as a pre-handler `413`, and a body the handler never reads is never counted.
**Example:**
```ts
app.post("/", async (event) => {
assertBodySize(event, 10 * 1024 * 1024); // 10MB
const data = await event.req.formData();
});
```
### `readBody(event, options?)`
Reads request body and tries to parse using JSON.parse or URLSearchParams.
By default the body is parsed as JSON (falling back to URL-encoded parsing when the `Content-Type` is `application/x-www-form-urlencoded`). Other body types, such as `multipart/form-data`, must be opted into explicitly via `options.type` and are never auto-detected from the request headers.
**Example:**
```ts
app.post("/", async (event) => {
const body = await readBody(event);
});
```
**Example:**
```ts
app.post("/upload", async (event) => {
const body = await readBody(event, { type: "formData" });
});
```
### `readValidatedBody(event, validate)`
Tries to read the request body via `readBody`, then uses the provided validation schema or function and either throws a validation error or returns the result.
You can use a simple function to validate the body or use a Standard-Schema compatible library like `zod` to define a schema.
**Example:**
```ts
function validateBody(body: any) {
return typeof body === "object" && body !== null;
}
app.post("/", async (event) => {
const body = await readValidatedBody(event, validateBody);
});
```
**Example:**
```ts
import { z } from "zod";
const objectSchema = z.object({
name: z.string().min(3).max(20),
age: z.number({ coerce: true }).positive().int(),
});
app.post("/", async (event) => {
const body = await readValidatedBody(event, objectSchema);
});
```
**Example:**
```ts
import * as v from "valibot";
app.post("/", async (event) => {
const body = await readValidatedBody(
event,
v.object({
name: v.pipe(v.string(), v.minLength(3), v.maxLength(20)),
age: v.pipe(v.number(), v.integer(), v.minValue(1)),
}),
{
onError: ({ issues }) => ({
statusText: "Custom validation error",
message: v.summarize(issues),
}),
},
);
});
```
## Query (HTTP `QUERY` method)
Utilities for the [HTTP `QUERY` method (RFC 10008)](https://www.rfc-editor.org/rfc/rfc10008): advertise the query formats a resource accepts and validate the request `Content-Type`.
### `appendAcceptQuery(event, mediaTypes)`
Advertise the query formats a resource accepts by setting the `Accept-Query` response header (RFC 10008, HTTP `QUERY` method).
The media types are serialized as a [Structured Fields](https://www.rfc-editor.org/rfc/rfc8941) List: the base media type becomes a token and any `;name=value` parameters are emitted with their values as quoted strings.
**Example:**
```ts
app.query("/search", (event) => {
appendAcceptQuery(event, ["application/sql;charset=UTF-8", "application/jsonpath"]);
// Accept-Query: application/sql;charset="UTF-8", application/jsonpath
return handleSearch(event);
});
```
### `requireContentType(event, acceptedTypes)`
Assert that the request `Content-Type` is present and one of the accepted media types, following the requirements of RFC 10008 for the HTTP `QUERY` method.
Throws:
- `400 Bad Request` if the `Content-Type` header is missing.
- `422 Unprocessable Content` if the `Content-Type` header is malformed.
- `415 Unsupported Media Type` if the media type is not accepted.
Accepted types may use wildcards: `*` / `*/*` match anything and `type/*` matches any subtype of `type`.
**Example:**
```ts
app.query("/search", async (event) => {
requireContentType(event, ["application/sql", "application/jsonpath"]);
const body = await readBody(event, { type: "text" });
// ...
});
```
## Cache
### `handleCacheHeaders(event, opts)`
Check request caching headers (`If-None-Match`, `If-Modified-Since`) and add caching headers (Last-Modified, ETag, Cache-Control).
Note: `public` is added by default, but never alongside a caller-supplied `private`/`no-store` directive, so passing `cacheControls: ["private"]` no longer produces a contradictory `public, private`.
## More Request Utils
### `assertMethod(event, expected, allowHead?)`
Asserts that the incoming request method is of the expected type using `isMethod`.
If the method is not allowed, it will throw a 405 error and include an `Allow` response header listing the permitted methods, as required by RFC 9110.
If `allowHead` is `true`, it will allow `HEAD` requests to pass if the expected method is `GET`.
**Example:**
```ts
app.get("/", (event) => {
assertMethod(event, "GET");
// Handle GET request, otherwise throw 405 error
});
```
### `getQuery(event)`
Get parsed query string object from the request URL.
To access the raw (unparsed) query string, for example to parse nested queries with a custom parser such as `qs`, use `event.url.search` directly.
**Example:**
```ts
app.get("/", (event) => {
const query = getQuery(event); // { key: "value", key2: ["value1", "value2"] }
const rawQuery = event.url.search; // "?key=value&key2=value1&key2=value2"
});
```
### `getRequestHost(event, opts: { xForwardedHost? })`
Get the request hostname.
If `xForwardedHost` is `true`, it will use the `x-forwarded-host` header if it exists.
If no host header is found, it will return an empty string.
**Security:** The returned host reflects the client-supplied `Host` (or `X-Forwarded-Host`) header and can be spoofed. Do not trust it for security decisions (CSRF/origin checks, cache keys, generating absolute links sent to other users) unless the `Host` value is pinned or validated upstream (e.g. an allow-list of expected hosts, or a reverse proxy that overwrites it).
**Example:**
```ts
app.get("/", (event) => {
const host = getRequestHost(event); // "example.com"
});
```
### `getRequestIP(event)`
Try to get the client IP address from the incoming request.
By default the address comes from `event.req.ip`: the connection peer, or the client resolved from the forwarded chain when the server is configured to trust an upstream proxy (e.g. srvx's `trustProxy`).
If `xForwardedFor` is `true`, the **first** entry of the `x-forwarded-for` header is returned instead, when the header exists.
If IP cannot be determined, it will default to `undefined`.
**Security:** `xForwardedFor` is opt-in because that first entry is client input. Proxies conventionally _append_ to the chain (nginx `$proxy_add_x_forwarded_for`, most CDNs, and h3's own {@link proxy} util), so a value sent by the client stays at the left of the chain and is exactly what this returns — letting any caller choose their own address and defeat IP allow-lists, rate limiting, geo checks, and audit logs. Enabling it also _overrides_ `event.req.ip`, discarding an address the server already resolved correctly. Prefer configuring the server to trust your proxy (srvx `trustProxy` walks the chain from the right, past trusted hops) and leave this option off; only enable it when an upstream you control always overwrites `x-forwarded-for` on every request.
**Example:**
```ts
app.get("/", (event) => {
const ip = getRequestIP(event); // "192.0.2.0"
});
```
### `getRequestProtocol(event, opts: { xForwardedProto? })`
Get the request protocol.
If `xForwardedProto` is `true`, it will use the `x-forwarded-proto` header if it exists. When the header contains a comma-separated list of protocols, the first entry is used.
Note: This header is opt-in (default `false`) since it can be spoofed by clients. Only enable it when your application runs behind a trusted reverse proxy or CDN that sets this header. This default was changed to match `getRequestHost` (`xForwardedHost`) and `getRequestIP` (`xForwardedFor`).
If protocol cannot be determined, it will default to "http".
**Example:**
```ts
app.get("/", (event) => {
const protocol = getRequestProtocol(event); // "https"
});
```
### `getRequestURL(event, opts: { xForwardedHost?, xForwardedProto? })`
Generated the full incoming request URL.
If `xForwardedHost` is `true`, it will use the `x-forwarded-host` header if it exists.
If `xForwardedProto` is `true`, it will use the `x-forwarded-proto` header if it exists.
**Security:** The `.origin` and `.host` of the returned URL are derived from the client-supplied `Host` (or `X-Forwarded-Host`) header and can be spoofed. Do not trust them for security decisions (CSRF/origin checks, cache keys, generating absolute links sent to other users) unless the `Host` value is pinned or validated upstream (e.g. an allow-list of expected hosts, or a reverse proxy that overwrites it). The `.pathname` and `.search` are not derived from the spoofable host, but remain untrusted client input — validate or encode them for their eventual sink (e.g. filesystem lookups, HTML output, downstream queries).
**Example:**
```ts
app.get("/", (event) => {
const url = getRequestURL(event); // "https://example.com/path"
});
```
### `getRouterParam(event, name, opts: { decode? })`
Get a matched route param by name.
If `decode` option is `true`, it will decode the matched route param (like `decodeURIComponent`), except encoded path separators (`%2f`, `%5c`) are kept encoded so decoding can never reintroduce a `/` or `\` the router never matched.
**Example:**
```ts
app.get("/", (event) => {
const param = getRouterParam(event, "key");
});
```
### `getRouterParams(event, opts: { decode? })`
Get matched route params.
By default params are returned exactly as they appeared in the URL path, still percent-encoded.
With `decode: true` each param is decoded **once** (like `decodeURIComponent`), except encoded path separators (`%2f`, `%5c`, at any `%25`-nesting depth) which are left in their encoded form so decoding can never reintroduce a `/` or `\` the router never matched.
A single decode is not the same as "fully decoded": `%25XX` decodes to the literal text `%XX`, so the result can still contain percent-escapes — including dot segments (`%252e%252e` -> `%2e%2e`) and control characters (`%2500` -> `%00`). **Do not decode the result again**: a second pass turns those back into traversal (`../`) and separators the routing and middleware layers never saw. Treat the returned string as final and validate it as-is.
**Example:**
```ts
app.get("/", (event) => {
const params = getRouterParams(event); // { key: "value" }
});
```
**Example:**
```ts
// GET /files/%252e%252e/x
app.get("/files/**:rest", (event) => {
getRouterParams(event); // { rest: "%252e%252e/x" }
getRouterParams(event, { decode: true }); // { rest: "%2e%2e/x" } — still encoded, do not decode again
});
```
### `getValidatedQuery(event, validate)`
Get the query param from the request URL validated with validate function.
You can use a simple function to validate the query object or use a Standard-Schema compatible library like `zod` to define a schema.
**Example:**
```ts
app.get("/", async (event) => {
const query = await getValidatedQuery(event, (data) => {
return "key" in data && typeof data.key === "string";
});
});
```
**Example:**
```ts
import { z } from "zod";
app.get("/", async (event) => {
const query = await getValidatedQuery(
event,
z.object({
key: z.string(),
}),
);
});
```
**Example:**
```ts
import * as v from "valibot";
app.get("/", async (event) => {
const params = await getValidatedQuery(
event,
v.object({
key: v.string(),
}),
{
onError: ({ issues }) => ({
statusText: "Custom validation error",
message: v.summarize(issues),
}),
},
);
});
```
### `getValidatedRouterParams(event, validate)`
Get matched route params and validate with validate function.
If `decode` option is `true`, params are decoded **once** exactly as described in {@link getRouterParams} — path separators stay encoded, other escapes decode a single level, and the validated value can still contain `%XX`. Validate it as-is; do not decode it again.
You can use a simple function to validate the params object or use a Standard-Schema compatible library like `zod` to define a schema.
**Example:**
```ts
app.get("/:key", async (event) => {
const params = await getValidatedRouterParams(event, (data) => {
return "key" in data && typeof data.key === "string";
});
});
```
**Example:**
```ts
import { z } from "zod";
app.get("/:key", async (event) => {
const params = await getValidatedRouterParams(
event,
z.object({
key: z.string(),
}),
);
});
```
**Example:**
```ts
import * as v from "valibot";
app.get("/:key", async (event) => {
const params = await getValidatedRouterParams(
event,
v.object({
key: v.pipe(v.string(), v.picklist(["route-1", "route-2", "route-3"])),
}),
{
decode: true,
onError: ({ issues }) => ({
statusText: "Custom validation error",
message: v.summarize(issues),
}),
},
);
});
```
### `isMethod(event, expected, allowHead?)`
Checks if the incoming request method is of the expected type.
If `allowHead` is `true`, it will allow `HEAD` requests to pass if the expected method is `GET`.
**Example:**
```ts
app.get("/", (event) => {
if (isMethod(event, "GET")) {
// Handle GET request
} else if (isMethod(event, ["POST", "PUT"])) {
// Handle POST or PUT request
}
});
```
### `requestWithBaseURL(req, base, options: { url?: URL })`
Create a lightweight request proxy with the base path stripped from the URL pathname.
`options.url` is the parsed request URL to strip `base` from, in place of parsing `req.url`. Pass `event.url` whenever there is an event: for a non-canonical path it holds the canonicalized form the parent matched `base` against, while `req.url` still holds the wire form, and slicing one by an offset derived from the other is how mount prefixes desync.
### `requestWithURL(req, url)`
Create a lightweight request proxy that overrides only the URL.
Avoids cloning the original request (no `new Request()` allocation).
### `toRequest(input, options?)`
Convert input into a web [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request).
If input is a relative URL, it will be normalized into a full path based on the `host` header.
If input is already a Request and no options are provided, it will be returned as-is.
**Security:** The `host` header is client input. It is only used as the authority of the synthesized URL (falling back to `localhost` when absent or malformed) and can never widen into the path, and `x-forwarded-proto` is ignored, so the scheme is always `http`. Pass an absolute URL to control the origin.
### `getRequestFingerprint(event, opts)`
Get a unique fingerprint for the incoming request.
---
# Response
> H3 response utilities.
## Event Stream
### `EventStream()`
### `isEventStream(input)`
## Sanitize
### `sanitizeStatusCode(statusCode?, defaultStatusCode)`
Make sure the status code is a valid HTTP status code.
### `sanitizeStatusMessage(statusMessage)`
Make sure the status message is safe to use in a response.
Allowed characters: horizontal tabs, spaces or visible ascii characters: https://www.rfc-editor.org/rfc/rfc7230#section-3.1.2
## Serve Static
### `serveStatic(event, options)`
Dynamically serve static assets based on the request path.
**Security — path traversal:** `serveStatic` resolves `.`/`..` segments but deliberately keeps encoded separators (`%2f`, `%5c`) percent-encoded in the `id` it passes to `getMeta`/`getContents`, exactly as `event.url.pathname` does. The `id` therefore has the same segment structure the router and pathname-scoped `use()` guards matched on: `/private%5cx` stays one opaque segment and cannot be served as `/private/x` past a `use("/private/**")` guard. Resolve the `id` against your asset root as an opaque string — a backend that decodes it re-introduces separators and re-opens the hole.
A **non-canonical pathname is not served** (404, or falls through when `fallthrough` is set): more than one leading separator (`//private/x`, `/\\private/x`) or a dot segment that survived URL canonicalization, which means one spelled with `%25`-nested escapes (`/pub/%252e%252e/private/x`). Both dispatch to a catch-all route while missing a narrower `use("/private/**")` guard, and the only `id` `serveStatic` could build from them resolves back into the guarded path. Assets are reachable under their canonical spelling — the one routing and `use()` guards match on — only.
Everything else is decoded once for the on-disk lookup, so a file's real name reaches the backend: `/50%25.png` → `/50%.png`, `/a%20b` → `/a b`, and one `%25` level is peeled off a nested separator (`/a%252fb` → `/a%2fb`, still a literal `%2f`, never a boundary). RFC 3986's reserved set stays encoded, so an `id` can never grow a `?` or `#` that would truncate it in a URL.
Two things `serveStatic` cannot enforce for filesystem-backed assets: **case-insensitive filesystems** (macOS, Windows) need both sides of any allow/deny check case-folded (otherwise `/SECRET.env` slips past a check for `/secret.env`), and **symlinks** need the resolved path re-asserted against the asset root after following links (e.g. `realpath(target)`).
## More Response Utils
### `html(first)`
### `iterable(iterable)`
Iterate a source of chunks and send back each chunk in order. Supports mixing async work together with emitting chunks.
Each chunk must be a string or a buffer.
For generator (yielding) functions, the returned value is treated the same as yielded values.
The first chunk is awaited before the response is created, so status and headers staged while producing it (`event.res.status`, `event.res.headers`) are still applied. Everything set after the first chunk is ignored — headers are already on the wire by then. (Returning a raw `ReadableStream` gives no such window: its response is created before the stream is read.)
**Example:**
```ts
return iterable(async function* work() {
// Open document body
yield "\nExecuting...
\n";
// Do work ...
for (let i = 0; i < 1000; i++) {
await delay(1000);
// Report progress
yield `- Completed job #`;
yield i;
yield `
\n`;
}
// Close out the report
return `
`;
});
async function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
```
### `noContent(status)`
Respond with an empty payload.
**Example:**
```ts
app.get("/", () => noContent());
```
### `onDispose(event, cb)`
Register a callback that runs once the event is fully over: the response body finished streaming, the client disconnected, or the body errored — on every runtime, not just Node.js.
The callback receives `undefined` on normal completion, or the cancel/abort reason otherwise. Callbacks run in registration order after the global `onResponse` hook; sync throws and async rejections are absorbed (reported via `console.error` unless the app is configured with `silent`), and pending async callbacks are passed to `waitUntil`.
Registering after disposal invokes the callback immediately. Registration is only guaranteed to observe the end of the event when made during request handling (handler, middleware, or `onResponse`).
Note: this signals _"h3 is done with this event"_, not _"the client received the response"_ — for non-streaming bodies on non-Node.js runtimes it fires when the response is handed to the runtime. To react to a client disconnect _while still producing_ the response (for example to abort an upstream fetch), use `event.req.signal` instead.
**Example:**
```ts
app.get("/sse", (event) => {
const interval = setInterval(() => {}, 1000);
onDispose(event, () => clearInterval(interval));
// ... return a streaming response
});
```
### `raw(value)`
Mark a string as trusted, pre-escaped HTML so it is used by the {@link html} util **without** being escaped.
Only use this for markup you fully control — passing user input to `raw` re-introduces XSS risk.
**Example:**
```ts
// `heading` is trusted markup; `userName` is escaped automatically.
app.get("/", () => html`${raw(heading)}${userName}
`);
```
**Example:**
```ts
// Send a trusted markup string as-is:
app.get("/", () => html(raw("Hello, World!
")));
```
### `redirect(location, status, statusText?)`
Send a redirect response to the client.
It adds the `location` header to the response and sets the status code to 302 by default.
In the body, it sends a simple HTML page with a meta refresh tag to redirect the client in case the headers are ignored.
**Security:** If `location` derives from user input (query params, form fields, headers, etc.), validate it against an allow-list of permitted destinations before redirecting. Passing user-controlled values through unchecked creates an open redirect vulnerability. Prefer `redirectBack` for "return to previous page" flows, which only honors same-origin referers.
**Example:**
```ts
app.get("/", () => {
return redirect("https://example.com");
});
```
**Example:**
```ts
app.get("/", () => {
return redirect("https://example.com", 301); // Permanent redirect
});
```
### `redirectBack(event)`
Redirect the client back to the previous page using the `referer` header.
If the `referer` header is missing or is a different origin, it falls back to the provided URL (default `"/"`).
By default, only the **pathname** of the referer is used (query string and hash are stripped) to prevent spoofed referers from carrying unintended parameters. Set `allowQuery: true` to preserve the query string.
**Security:** The `fallback` value MUST be a trusted, hardcoded path — never use user input. Passing user-controlled values (e.g., query params) as `fallback` creates an open redirect vulnerability.
**Example:**
```ts
app.post("/submit", (event) => {
// process form...
return redirectBack(event, { fallback: "/form" });
});
```
### `writeEarlyHints(event, hints)`
Write `HTTP/1.1 103 Early Hints` to the client.
In runtimes that don't support early hints natively, this function falls back to setting response headers which can be used by CDN.
---
# Cookie
> H3 cookie utilities.
### `deleteChunkedCookie(event, name, serializeOptions?)`
Remove a set of chunked cookies by name.
### `deleteCookie(event, name, serializeOptions?)`
Remove a cookie by name.
### `getChunkedCookie(event, name)`
Get a chunked cookie value by name. Will join chunks together.
### `getCookie(event, name)`
Get a cookie value by name.
### `getValidatedCookies(event, validate, options?: { onError?: OnValidateError })`
### `parseCookies(event)`
Parse the request to get HTTP Cookie header string and returning an object of all cookie name-value pairs.
### `setChunkedCookie(event, name, value, options?)`
Set a cookie value by name. Chunked cookies will be created as needed.
### `setCookie(event, name, value, options?)`
Set a cookie value by name.
---
# Security
> H3 security utilities.
## Authentication
### `basicAuth(opts)`
Create a basic authentication middleware.
**Example:**
```ts
import { H3, serve, basicAuth } from "h3";
const auth = basicAuth({ password: "test" });
app.get("/", (event) => `Hello ${event.context.basicAuth?.username}!`, [auth]);
serve(app, { port: 3000 });
```
### `requireBasicAuth(event, opts)`
Apply basic authentication for current request.
**Example:**
```ts
import { defineHandler, requireBasicAuth } from "h3";
export default defineHandler(async (event) => {
await requireBasicAuth(event, { password: "test" });
return `Hello, ${event.context.basicAuth.username}!`;
});
```
## Session
### `clearSession(event, config)`
Clear the session data for the current request.
### `getSession(event, config)`
Get the session for the current request.
A request without a session gets a new one initialized in memory only — no `Set-Cookie` is issued until something is stored with {@link updateSession}, so reading the session (an auth check, for example) does not start one for anonymous visitors. Its `id` is therefore only stable across requests once the session has been written; use {@link useSession} to start one eagerly.
### `sealSession(event, config)`
Encrypt and sign the session data for the current request.
### `unsealSession(_event, config, sealed)`
Decrypt and verify the session data for the current request.
### `updateSession(event, config, update?)`
Update the session data for the current request.
### `useSession(event, config)`
Create a session manager for the current request.
Starts a session if the request does not carry one, persisting it so its id is stable across requests. Use {@link getSession} to read a session without starting one.
## Fingerprint
### `getRequestFingerprint(event, opts)`
Get a unique fingerprint for the incoming request.
## CORS
### `appendCorsHeaders(event, options)`
Append CORS headers to the response.
### `appendCorsPreflightHeaders(event, options)`
Append CORS preflight headers to the response.
### `handleCors(event, options)`
Handle CORS for the incoming request.
If the incoming request is a CORS preflight request, it will append the CORS preflight headers and send a 204 response.
If return value is not `false`, the request is handled and no further action is needed.
**Example:**
```ts
const app = new H3();
app.all("/", async (event) => {
const corsRes = handleCors(event, {
origin: "*",
preflight: {
statusCode: 204,
},
methods: "*",
});
if (corsRes !== false) {
return corsRes;
}
// Your code here
});
```
### `isCorsOriginAllowed(origin, options)`
Check if the origin is allowed.
### `isPreflightRequest(event)`
Check if the incoming request is a CORS preflight request.
## Path
### `isCanonicalPath(path, opts?)`
Whether `path` is already canonical under `opts` — i.e. {@link resolveDotSegments} would return it unchanged. Exact in both directions: `true` if and only if `resolveDotSegments(path, opts) === path`.
This is the resolver's own fast-path guard, exported so a caller that canonicalizes on a hot path (per-request scope or rule matching) can skip the call — and any work derived from it — without keeping its own copy of what the resolver decodes. Such a copy goes stale silently, and a missed canonicalization in a scope check is a bypass, not a perf bug.
Pass the same options as the later {@link resolveDotSegments} call, or stricter ones: `decodeSlashes`/`mergeSlashes` only add triggers, so `true` with both enabled implies `true` in every mode. Checking one mode and resolving in another voids the guarantee.
Takes a bare pathname. Like the resolver, it has no notion of a query or hash and scans one as if it were path, so `/a?next=/../b` is reported non-canonical (and would resolve to `/b`).
### `normalizeRoute(route)`
Normalize a route pattern into the canonical form h3 registers it under — the same shape as the `event.url.pathname` it will be matched against.
`app.on()`, `app.use(route, …)`, `app.mount()` and `removeRoute()` all apply this to the pattern they receive. Use it when registering patterns into a router of your own (e.g. a build-time compiled rou3 router) that is then matched against h3's `event.url.pathname`, so both sides agree on the string — a pattern that normalized differently could leave a route reachable while a guard registered with the same source string matches nothing.
A leading `/` is added if missing (`about` → `/about`), characters a request pathname always carries percent-encoded are encoded (`/café/**` → `/caf%C3%A9/**`), needless escapes are decoded the way h3 decodes them in the request pathname (`/%40handle` → `/@handle`; `%2F` and `%25` stay encoded), and `.`/`..` segments are resolved (`/a/b/../c` → `/a/c`). rou3 pattern syntax (`?`, `{`, `}`, `^`, `\`) is left verbatim — spell one percent-encoded to match it literally.
Idempotent. Throws on an absolute URL (`http://…`): a route pattern is a pathname, never a URL.
**Example:**
```ts
normalizeRoute("/について/**"); // "/%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6/**"
```
### `resolveDotSegments(path, opts?)`
Resolve `.` and `..` segments in a path, without ever escaping above the root `/`. The result is always an absolute path with a single leading `/`, so it can never be protocol-relative (`//host`).
Also decodes percent-encoded dot segments at any `%25`-nesting depth (`%2e`, `%252e`, ...) and normalizes `\` to `/`, so encoded or backslash-based traversal (e.g. `%2e%2e/`, `..\..\`) is caught the same way as a literal `../`.
`%2f`/`%5c` (encoded path separators) are left untouched by default — see {@link ResolveDotSegmentsOptions.decodeSlashes}.
Only `.`/`..` resolution and the decodes above alter the string; every other percent-encoding (`%20`, non-ASCII, `%3A`, and any `%2e` not forming a whole segment) is left intact, so the result stays in the same representation as `event.url.pathname` and matches routes/rules consistently. A trailing `.`/`..` resolves to a directory and keeps its trailing slash (`/a/b/..` -> `/a/`, `/a/.` -> `/a/`), per RFC 3986 §5.2.4 and matching what a WHATWG/nginx downstream resolves — so a scope check sees the directory form, not its file-form sibling. Interior empty segments are preserved (`/a//b` stays `/a//b`) — like WHATWG, this never merges slashes, so empty segments survive rather than collapsing. The one exception is a _leading_ run: it is always clamped to a single `/` (WHATWG would keep `//host`), so only the leading slash is guaranteed single and a consumer doing exact prefix matching should normalize its allowlist the same way. To collapse interior runs too (the reading a slash-merging downstream resolves), see {@link ResolveDotSegmentsOptions.mergeSlashes}.
## Route params
Route params reach your handler in the form they had in the URL path — percent-encoded. `getRouterParams(event, { decode: true })` (and `getValidatedRouterParams` with the same option) applies **one** decode pass, not a full normalization:
- Encoded path separators (`%2f`, `%5c`, at any `%25`-nesting depth: `%252f`, `%25252f`, ...) are **never** decoded. A raw `/` or `\` can never appear in a param that the router matched as one segment, so a param cannot silently gain a path boundary that routing and middleware never saw.
- Every other escape decodes exactly one level. Because `%25` is itself an escape, `%25XX` decodes to the literal text `%XX` — so the result can still contain percent-escapes.
```ts
app.get("/files/**:rest", (event) => {
// GET /files/%252e%252e/x
getRouterParams(event); // { rest: "%252e%252e/x" }
getRouterParams(event, { decode: true }); // { rest: "%2e%2e/x" }
// GET /files/%2500
getRouterParams(event, { decode: true }); // { rest: "%00" }
// GET /files/a%252fb — separators stay encoded at every depth
getRouterParams(event, { decode: true }); // { rest: "a%252fb" }
});
```
> [!IMPORTANT]
> Do not decode the returned value again. A second `decodeURIComponent` turns `%2e%2e/x` into `../x` and `%00` into a NUL byte — traversal and control characters that were not visible to routing or to any pathname-based middleware. Validate the value as returned, and if it will be used as a filesystem or upstream path, resolve it with [`resolveDotSegments`](#resolvedotsegmentspath-opts) rather than by decoding further.
---
# Proxy
> H3 proxy utilities.
### `fetchWithEvent(event, url, init?)`
Make a fetch request carrying the event's context.
Behavior depends on the target:
An **internal** `url` (starting with `/`) is dispatched via `event.app.fetch()` (sub-request) and never leaves the process. It inherits the incoming request's filtered headers (via `getProxyRequestHeaders`) and runtime metadata (`ip`, `waitUntil`, ...). It always resolves against the app's own origin: a leading separator run (`//host/x`, `/\host/x`, and C0-interleaved forms like `/\thost/x` that the URL parser strips down to one) is collapsed to a single `/` rather than read as an authority.
An **external** `url` is sent with native `fetch(url, init)` **unchanged** — the event's headers and context are _not_ inherited (forwarding cookies or authorization to arbitrary hosts would be unsafe). A streamed `init.body` is given `duplex: "half"` when unset, which Node's `fetch` requires.
**Security:** Never pass unsanitized user input as the `url`. Callers are responsible for validating and restricting the URL.
### `getProxyRequestHeaders(event)`
Get the request headers object without headers known to cause issues when proxying.
### `proxy(event, target, opts)`
Make a proxy request to a target URL and send the response back to the client.
If the `target` starts with `/`, the request is dispatched internally via `event.app.fetch()` (sub-request) and never leaves the process. This bypasses any external security layer (reverse proxy auth, IP allowlisting, mTLS).
Upstream 3xx responses are passed through to the client by default rather than followed. Set `fetchOptions: { redirect: "follow" }` to follow them instead — but following a redirect with a streamed request body can fail, since the body cannot be replayed once consumed. (Internal sub-requests via `event.app.fetch()` never follow redirects.)
**Limitations** (inherited from `fetch`): upstream response bodies are always decompressed (compression is not preserved end-to-end), the `host` header is rewritten to the target (preserving it via `forwardHeaders: ["host"]` works on Node.js but may be ignored on other runtimes), and unix sockets, TLS options, or connection agents require a runtime-specific escape hatch (e.g. undici's `dispatcher` in `fetchOptions` on Node.js). On browser and service-worker runtimes, `redirect: "manual"` produces an unrelayable opaque-redirect for external targets (a `502` is returned) — set `fetchOptions: { redirect: "follow" }` there.
**Security:** Never pass unsanitized user input as the `target`. Callers are responsible for validating and restricting the target URL (e.g. allowlisting hosts, blocking internal paths, enforcing protocol).
**Credential forwarding:** `proxy` does not forward the incoming request's headers automatically — only headers the caller explicitly passes via `opts.headers` (or `fetchOptions.headers`) are sent, verbatim. Do not pass the client's `Cookie` or `Authorization` headers through to an upstream you do not fully trust. Note that `opts.filterHeaders` has no effect here — it is only applied by `proxyRequest` (which does forward the incoming headers and offers `filterHeaders: ["cookie", "authorization"]` as the mitigation).
### `proxyRequest(event, target, opts)`
Proxy the incoming request to a target URL.
If the `target` starts with `/`, the request is handled internally by the app router via `event.app.fetch()` instead of making an external HTTP request. Such a target always resolves against the app's own origin: a leading separator run (`//host/x`, `/\host/x`, and C0-interleaved forms like `/\thost/x` that the URL parser strips down to one) is collapsed to a single `/` rather than read as an authority.
The request body is streamed to the target without buffering. Per the Fetch standard, a request body can only be consumed once, so reading it beforehand (e.g. via `readBody()`, `readFormData()`, or body-reading middleware) locks the stream and proxying fails. If you need to inspect the body and still proxy it, read from a clone and leave the original event untouched.
Upstream 3xx responses are passed through to the client by default rather than followed. Set `fetchOptions: { redirect: "follow" }` to follow them instead — but following a redirect with a streamed request body can fail, since the body cannot be replayed once consumed.
**Security:** Never pass unsanitized user input as the `target`. Callers are responsible for validating and restricting the target URL (e.g. allowlisting hosts, blocking internal paths, enforcing protocol). Consider using `bodyLimit()` middleware to prevent large request bodies from consuming excessive resources when proxying untrusted input.
**Credential forwarding:** the incoming request's `Cookie` and `Authorization` headers are forwarded to the `target` verbatim. This is the correct behavior for a same-trust reverse proxy, but leaks the client's credentials to any upstream you do not fully trust. When proxying to a not-fully-trusted upstream, strip them with `filterHeaders: ["cookie", "authorization"]`. (This differs from `fetchWithEvent`, which never forwards the event's headers to an external URL.)
**Example:**
```ts
app.all("/proxy", async (event) => {
const body = await event.req.clone().json(); // read from the clone
// ...inspect body...
return proxyRequest(event, "/target"); // original stream still intact
});
```
---
# MCP
> H3 MCP related utils.
### `defineJsonRpcHandler()`
Creates an H3 event handler that implements the JSON-RPC 2.0 specification.
**Security defaults:** requests must have a JSON `Content-Type` (CSRF, see `validateContentType`), cross-origin requests are rejected (CSRF and DNS rebinding, see `allowedOrigins`), and batches are capped at 50 requests (fan-out amplification, see `maxBatchSize`).
**Example:**
```ts
app.post(
"/rpc",
defineJsonRpcHandler({
methods: {
echo: ({ params }, event) => {
return `Received \`${params}\` on path \`${event.url.pathname}\``;
},
sum: ({ params }, event) => {
return params.a + params.b;
},
},
}),
);
```
### `defineJsonRpcWebSocketHandler()`
Creates an H3 event handler that implements JSON-RPC 2.0 over WebSocket.
This is an opt-in feature that allows JSON-RPC communication over WebSocket connections for bi-directional messaging. Each incoming WebSocket text message is processed as a JSON-RPC request, and responses are sent back to the peer.
**Security:** unlike `defineJsonRpcHandler()`, this does not check the request `Origin`. WebSocket upgrades are not subject to CORS, so a page on any origin can open a connection carrying the visitor's cookies (cross-site WebSocket hijacking). Validate `Origin` in the `upgrade` hook and throw a `Response` to abort the connection.
**Example:**
```ts
app.get(
"/rpc/ws",
defineJsonRpcWebSocketHandler({
methods: {
echo: ({ params }) => {
return `Received: ${Array.isArray(params) ? params[0] : params?.message}`;
},
sum: ({ params }) => {
return params.a + params.b;
},
},
}),
);
```
**Example:**
```ts
// With additional WebSocket hooks
app.get(
"/rpc/ws",
defineJsonRpcWebSocketHandler({
methods: {
greet: ({ params }) => `Hello, ${params.name}!`,
},
hooks: {
open(peer) {
console.log(`Peer connected: ${peer.id}`);
},
close(peer, details) {
console.log(`Peer disconnected: ${peer.id}`, details);
},
},
}),
);
```
---
# More utils
> More H3 utilities.
## Base
### `withBase(base, input)`
Returns a new event handler that removes the base url of the event before calling the original handler.
**Example:**
```ts
const api = new H3()
.get("/", () => "Hello API!");
const app = new H3();
.use("/api/**", withBase("/api", api.handler));
```
## Event
### `getEventContext(event)`
Gets the context of the event, if it does not exists, initializes a new context on `req.context`.
### `isEvent(input)`
Checks if the input is an H3Event object.
### `isHTTPEvent(input)`
Checks if the input is an object with `{ req: Request }` signature.
### `mockEvent(_request, options?)`
## Middleware
### `bodyLimit(limit)`
Define a middleware that limits the request body size to the specified limit.
The limit is enforced as the body is read (see {@link assertBodySize}), so an oversized body surfaces as a `413` Request Entity Too Large error when the handler consumes it (an honest oversized `Content-Length` is still rejected up-front). A body the handler never reads is not counted. If you need custom handling, use `assertBodySize` directly.
### `onError(hook)`
Define a middleware that runs when an error occurs.
You can return a new Response from the handler to gracefully handle the error.
### `onRequest(hook)`
Define a middleware that runs on each request.
### `onResponse(hook)`
Define a middleware that runs after Response is generated.
You can return a new Response from the handler to replace the original response.
## WebSocket
### `defineWebSocket(hooks)`
Define WebSocket hooks.
**Example:**
```ts
const hooks = defineWebSocket({
open: (peer) => peer.send("Welcome!"),
message: (peer, message) => peer.send(message.text()),
close: (peer) => console.log("closed", peer),
});
```
### `defineWebSocketHandler(http?)`
Define WebSocket event handler.
By default, non-upgrade (plain HTTP) requests receive a `426 Upgrade Required` response. Pass an `http` handler to serve those requests instead, allowing the same route to handle both WebSocket upgrades and regular HTTP requests. WebSocket upgrade requests always go to `hooks`.
Note: the `http` handler only handles non-upgrade requests. To reject or customize the upgrade handshake itself, use the crossws `upgrade` hook instead.
**Example:**
```ts
// WebSocket-only route (non-upgrade requests get `426 Upgrade Required`)
app.get(
"/_ws",
defineWebSocketHandler({
message: (peer, message) => peer.send(message.text()),
}),
);
```
**Example:**
```ts
// Handle both WebSocket upgrades and plain HTTP on the same route
app.get(
"/_ws",
defineWebSocketHandler(
{ message: (peer, message) => peer.send(message.text()) },
() => "Send a WebSocket upgrade request to connect.",
),
);
```
## Adapters
### `defineNodeHandler(handler)`
### `defineNodeMiddleware(handler)`
### `fromNodeHandler(handler)`
### `fromWebHandler(handler)`
---
# Community
> H3 utils from community.
You can use external H3 event utilities made by the community.
This section is placeholder for any new H3 version 2 compatible community library.
> [!TIP]
> 💛 PR is more than welcome to list yours.
## `apitally`
[Apitally](https://apitally.io/h3) is a simple API monitoring, analytics, and request logging tool with a plugin for H3. See setup guide [here](https://docs.apitally.io/frameworks/h3).
:read-more{to="https://github.com/apitally/apitally-js"}
## `H3ravel Framework`
[H3ravel Framework](https://h3ravel.toneflix.net) is a modern TypeScript runtime-agnostic web framework built on top of H3, designed to bring the elegance and developer experience of Laravel PHP to the JavaScript ecosystem. See the getting started guide [here](https://h3ravel.toneflix.net/guide/get-started).
:read-more{to="https://github.com/h3ravel"}
## `Intlify`
[Intlify](https://intlify.dev/) is a project that aims to improve Developer Experience in software internationalization. That project provides server-side frameworks, middleware, and utilities. About those, see the [here](https://github.com/intlify/srvmid)
:read-more{to="https://github.com/intlify/srvmid"}
## `Clear Router`
Laravel-style routing system for H3 and Express.js. Clean route definitions, middleware support, and controller bindings with full TypeScript support.
:read-more{to="https://github.com/toneflix/clear-router"}
## `unjwt`
`unjwt` is a collection of low-level JWT utilities (JWS, JWE, JWK) built on the Web Crypto API, with zero runtime dependencies. It includes a dedicated H3 v2 adapter for header and cookie-based session management with support for encrypted (JWE) and signed (JWS) tokens.
:read-more{to="https://github.com/sandros94/unjwt"}
## `Arkstack`
[Arkstack](https://arkstack.toneflix.net) is a runtime-agnostic TypeScript backend framework for building structured, production-ready server applications with first-class support for H3.
:read-more{to="https://arkstack.toneflix.net/guide/getting-started"}
---
# Examples
> Common examples for h3.
::read-more{to="https://github.com/h3js/h3/tree/main/examples"}
Check [`examples/` dir](https://github.com/h3js/h3/tree/main/examples) for more examples.
::
**Examples:**
- [Cookies](/examples/handle-cookie)
- [HTTP QUERY Method](/examples/handle-query)
- [Session](/examples/handle-session)
- [Static Assets](/examples/serve-static-assets)
- [Streaming Response](/examples/stream-response)
- [Validation](/examples/validate-data)
---
# Cookies
> Use cookies to store data on the client.
Handling cookies with H3 is straightforward. There is three utilities to handle cookies:
- `setCookie` to attach a cookie to the response.
- `getCookie` to get a cookie from the request.
- `deleteCookie` to clear a cookie from the response.
## Set a Cookie
To set a cookie, you need to use `setCookie` in an event handler:
```ts
import { setCookie } from "h3";
app.use(async (event) => {
setCookie(event, "name", "value", { maxAge: 60 * 60 * 24 * 7 });
return "";
});
```
In the options, you can configure the [cookie flags](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie):
- `maxAge` to set the expiration date of the cookie in seconds.
- `expires` to set the expiration date of the cookie in a `Date` object.
- `path` to set the path of the cookie.
- `domain` to set the domain of the cookie.
- `secure` to set the `Secure` flag of the cookie.
- `httpOnly` to set the `HttpOnly` flag of the cookie.
- `sameSite` to set the `SameSite` flag of the cookie.
:read-more{to="/utils"}
## Get a Cookie
To get a cookie, you need to use `getCookie` in an event handler.
```ts
import { getCookie } from "h3";
app.use(async (event) => {
const name = getCookie(event, "name");
// do something...
return "";
});
```
This will return the value of the cookie if it exists, or `undefined` otherwise.
## Delete a Cookie
To delete a cookie, you need to use `deleteCookie` in an event handler:
```ts
import { deleteCookie } from "h3";
app.use(async (event) => {
deleteCookie(event, "name");
return "";
});
```
The utility `deleteCookie` is a wrapper around `setCookie` with the value set to `""` and the `maxAge` set to `0`.
This will erase the cookie from the client.
---
# HTTP `QUERY` Method
> Accept safe, cacheable requests that carry a query in the body.
The [HTTP `QUERY` method (RFC 10008)](https://www.rfc-editor.org/rfc/rfc10008) is like `GET` — **safe, idempotent, and cacheable** — but carries a query in the request **body** with a `Content-Type`. It's the standard answer to "I need a GET, but my query is too large or too structured for the URL".
H3 supports `QUERY` as a first-class method via [`app.query()`](/guide/basics/routing#http-query-method), plus two helper utilities.
## Register a `QUERY` Handler
Read the request body just like you would for a `POST`:
```ts
import { readBody } from "h3";
app.query("/books", async (event) => {
const query = await readBody(event, { type: "text" });
return runSearch(query);
});
```
Because `QUERY` carries an attacker-controllable body, [body-size limits](/utils/request#assertbodysizeevent-limit) apply just like `POST`.
## Advertise Accepted Formats
Use [`appendAcceptQuery`](/utils/request#appendacceptqueryevent-mediatypes) to tell clients which query formats a resource understands. It sets the `Accept-Query` response header (a [Structured Fields](https://www.rfc-editor.org/rfc/rfc8941) List), and can be set on a plain `GET` too so clients can discover formats before sending a `QUERY`:
```ts
import { appendAcceptQuery } from "h3";
app.get("/books", (event) => {
appendAcceptQuery(event, ["application/sql", "application/jsonpath"]);
// Accept-Query: application/sql, application/jsonpath
return "Send a QUERY request with a SQL or JSONPath body.";
});
```
## Validate the `Content-Type`
Use [`requireContentType`](/utils/request#requirecontenttypeevent-acceptedtypes) to enforce the RFC's error semantics. It returns the matched media type, or throws `400` (missing), `415` (unsupported), or `422` (malformed):
```ts
import { requireContentType, readBody } from "h3";
app.query("/books", async (event) => {
const type = requireContentType(event, ["application/sql", "application/jsonpath"]);
const query = await readBody(event, { type: "text" });
return runQuery(type, query);
});
```
## Offer a Cacheable `GET` Alternative
A `QUERY` response is not addressable by URL, so browsers and CDNs can't cache it. RFC 10008 suggests pointing clients at an equivalent, cacheable `GET` via the `Content-Location` header. Stash the result under a stable id and let a client repeat the query with an ordinary, HTTP-cacheable `GET`:
```ts
app.query("/books", async (event) => {
const result = runQuery(type, query);
const id = queryId(type, query); // stable hash of the query
cache.set(id, result);
event.res.headers.set("content-location", `/books/${id}`);
return result;
});
```
## Full Example
A self-contained, runnable demo — a `/books` resource that accepts SQL-ish and JSONPath queries, validates the `Content-Type`, and advertises a cacheable `GET` alternative. It also serves a small interactive page at `/`.
::read-more{to="https://github.com/h3js/h3/tree/main/examples/query.mjs"}
See the full [`examples/query.mjs`](https://github.com/h3js/h3/tree/main/examples/query.mjs) source, or run it locally with `node examples/query.mjs`.
::
> [!NOTE]
> Unlike `GET`, `QUERY` is **not** CORS-safelisted, so browsers send a preflight. If you pass an explicit `methods` allowlist to [`handleCors`](/utils/security#handlecorsevent-options), include `"QUERY"`.
---
# Sessions
> Remember your users using a session.
A session is a way to remember users using cookies. It is a very common method for authenticating users or saving data about them, such as their language or preferences on the web.
H3 provides many utilities to handle sessions:
- `useSession` initializes a session and returns a wrapper to control it.
- `getSession` retrieves the current user session, without starting one.
- `updateSession` updates the data of the current session.
- `clearSession` clears the current session.
Most of the time, you will use `useSession` to manipulate the session.
## Initialize a Session
To initialize a session, you need to use `useSession` in an [event handler](/guide/basics/handler):
```js
import { useSession } from "h3";
app.use(async (event) => {
const session = await useSession(event, {
password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9",
});
// do something...
});
```
> [!WARNING]
> The `password` seals every session cookie, and its **entropy is the real security boundary**. A stolen session cookie carries the salt and integrity digest in plaintext, so a weak or guessable password can be brute-forced offline — increasing PBKDF2 iterations only slows this, it does not fix a low-entropy secret. Always generate the password from a cryptographically secure random source, for example:
>
> ```sh
> node -e "console.log(require('node:crypto').randomBytes(32).toString('base64'))"
> ```
>
> The examples below use a hardcoded value so they stay readable, but in a real app load a randomly generated secret of at least 32 characters from an environment variable such as `process.env.SESSION_PASSWORD`, and never commit it to source control. A guessable passphrase (even one ≥32 characters) is not safe.
This will initialize a session and return an header `Set-Cookie` with a cookie named `h3` and an encrypted content.
If the request contains a cookie named `h3` or a header named `x-h3-session`, the session will be initialized with the content of the cookie or the header.
> [!NOTE]
> The header take precedence over the cookie.
## Get Data from a Session
To get data from a session, we will still use `useSession`. Under the hood, it will use `getSession` to get the session.
```js
import { useSession } from "h3";
app.use(async (event) => {
const session = await useSession(event, {
password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9",
});
return session.data;
});
```
Data are stored in the `data` property of the session. If there is no data, it will be an empty object.
## Add Data to a Session
To add data to a session, we will still use `useSession`. Under the hood, it will use `updateSession` to update the session.
```js
import { useSession } from "h3";
app.use(async (event) => {
const session = await useSession(event, {
password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9",
});
const count = (session.data.count || 0) + 1;
await session.update({
count: count,
});
return count === 0 ? "Hello world!" : `Hello world! You have visited this page ${count} times.`;
});
```
What is happening here?
We try to get a session from the request. If there is no session, a new one will be created. Then, we increment the `count` property of the session and we update the session with the new value. Finally, we return a message with the number of times the user visited the page.
Try to visit the page multiple times and you will see the number of times you visited the page.
> [!NOTE]
> If you use a CLI tool like `curl` to test this example, you will not see the number of times you visited the page because the CLI tool does not save cookies. You must get the cookie from the response and send it back to the server.
## Clear a Session
To clear a session, we will still use `useSession`. Under the hood, it will use `clearSession` to clear the session.
```js
import { useSession } from "h3";
app.use("/clear", async (event) => {
const session = await useSession(event, {
password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9",
});
await session.clear();
return "Session cleared";
});
```
H3 will send a header `Set-Cookie` with an empty cookie named `h3` to clear the session.
## Options
When to use `useSession`, you can pass an object with options as the second argument to configure the session:
```js
import { useSession } from "h3";
app.use(async (event) => {
const session = await useSession(event, {
name: "my-session",
password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9",
cookie: {
httpOnly: true,
secure: true,
sameSite: "strict",
},
maxAge: 60 * 60 * 24 * 7, // 7 days
});
return session.data;
});
```
Every option is optional except `password`. The `name` option is worth calling out: it sets the cookie used to store the session and defaults to `h3`. H3 also reads the session from a request header derived from `name`, which it normalizes to lowercase as `x-${name.toLowerCase()}-session`, so the default name `h3` produces the `x-h3-session` header seen earlier. A mixed-case `name` like `MyApp` still resolves to a lowercase `x-myapp-session` header, while the cookie keeps the original casing. That default is why the earlier examples set a cookie named `h3`.
> [!NOTE]
> The session cookie defaults to `secure: true`, `httpOnly: true`, `sameSite: "lax"`, and `path: "/"`. Any of these can be overridden via `cookie`.
> [!NOTE]
> The `secure: true` option tells the browser to only store and send the cookie over HTTPS. When developing locally over plain HTTP, compliant browsers (notably Safari and iOS, and Chrome on some local domains) silently drop the cookie, so the session will not persist. Set `cookie: { secure: false }` during local development to work around this.
## Expiration
Sessions have two independent expiration controls, and you can use either or both:
- `maxAge` is an **absolute** lifetime, counted from when the session was created. It is reached however active the user is.
- `idleTimeout` is a **sliding** lifetime, counted from the last request. An active user stays signed in; an idle one is signed out.
```js
const session = await useSession(event, {
password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9",
idleTimeout: 60 * 30, // signed out after 30 minutes of inactivity...
maxAge: 60 * 60 * 24 * 7, // ...and after 7 days regardless
});
```
With `idleTimeout` set, H3 moves the idle window forward by resealing the session cookie with the reseal time stamped into it. `createdAt` is left untouched, which is what lets `maxAge` still act as a hard cap on top. The cookie `Expires` is set to whichever limit runs out first.
If you are coming from `express-session` or `koa-session`, `idleTimeout` is their `rolling` option. The difference is that it carries its own duration instead of reinterpreting `maxAge`, so enabling it does not cost you the absolute limit.
Resealing is the expensive part of a session, so H3 does not do it on every request: it reseals only once more than half the window has been used, and updating the session counts as a reseal. An active user therefore never gets signed out, but the recorded last-seen time can trail the real one by up to half the window:
```js
// idleTimeout: 60 * 30
// Sign-out happens 15 to 30 minutes after the last request, never later.
```
Halve `idleTimeout` if you need the shorter end of that range to be your real limit.
> [!NOTE]
> Only cookie sessions slide. A session sent through the `x-{name}-session` header cannot be resealed, so it expires `idleTimeout` after its seal was issued.
> [!IMPORTANT]
> Because the session lives in the cookie, a request that only reads the session writes it back when it slides the window. If such a request overlaps with one that writes the session, whichever response the browser applies last wins, so the write can be lost. Without `idleTimeout` a read-only request sets no cookie and cannot clobber a concurrent write.
> [!NOTE]
> A request that slides the window pays for an extra seal and puts a `Set-Cookie` header on its response — shared caches and CDNs often refuse to store those. Requests that only read the session inside the throttle window set no cookie at all.
The session cookie is also applied to error responses, so a request that throws still slides the window and still persists a session created during it.
## Use Multiple Sessions
Because each session is stored under its own `name`, you can run several independent sessions on the same request. They live in separate cookies and never overwrite each other, which is useful for keeping unrelated concerns apart, such as a long-lived auth session and a short-lived flash message:
```js
import { useSession } from "h3";
app.use(async (event) => {
const auth = await useSession(event, {
name: "auth",
password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9",
});
const flash = await useSession(event, {
name: "flash",
password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9",
});
await flash.update({ message: "Saved!" });
// `auth` and `flash` are backed by different cookies, so they stay separate
return { user: auth.data.user, flash: flash.data.message };
});
```
> [!NOTE]
> Give each session a distinct `name`. Two sessions that share a name share the same cookie, so the last write wins.
---
# Static Assets
> Serve static assets such as HTML, images, CSS, JavaScript, etc.
H3 can serve static assets such as HTML, images, CSS, JavaScript, etc.
To serve a static directory, you can use the `serveStatic` utility.
```ts
import { H3, serveStatic } from "h3";
const app = new H3();
app.use("/public/**", (event) => {
return serveStatic(event, {
getContents: (id) => {
// TODO
},
getMeta: (id) => {
// TODO
},
});
});
```
This does not serve any files yet. You need to implement the `getContents` and `getMeta` methods.
- `getContents` is used to read the contents of a file. It should return a `Promise` that resolves to the contents of the file or `undefined` if the file does not exist.
- `getMeta` is used to get the metadata of a file. It should return a `Promise` that resolves to the metadata of the file or `undefined` if the file does not exist.
They are separated to allow H3 to respond to `HEAD` requests without reading the contents of the file and to use the `Last-Modified` header.
## Read files
Now, create a `index.html` file in the `public` directory with a simple message and open your browser to http://localhost:3000. You should see the message.
Then, we can create the `getContents` and `getMeta` methods:
```ts
import { stat, readFile } from "node:fs/promises";
import { join } from "node:path";
import { H3, serve, serveStatic } from "h3";
const app = new H3();
app.use("/public/**", (event) => {
return serveStatic(event, {
indexNames: ["/index.html"],
getContents: (id) => readFile(join("public", id)),
getMeta: async (id) => {
const stats = await stat(join("public", id)).catch(() => {});
if (stats?.isFile()) {
return {
size: stats.size,
mtime: stats.mtimeMs,
};
}
},
});
});
serve(app);
```
The `getContents` reads the file and returns its contents, pretty simple. The `getMeta` uses `fs.stat` to get the file metadata. If the file does not exist or is not a file, it returns `undefined`. Otherwise, it returns the file size and the last modification time.
The file size and last modification time are used to create an etag to send a `304 Not Modified` response if the file has not been modified since the last request. This is useful to avoid sending the same file multiple times if it has not changed.
---
# Stream Response
> Stream response to the client.
Using stream responses It allows you to send data to the client as soon as you have it. This is useful for large files or long running responses.
## Create a Stream
To stream a response, you first need to create a stream using the [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) API:
```ts
const stream = new ReadableStream();
```
For the example, we will create a start function that will send a random number every 100 milliseconds. After 1000 milliseconds, it will close the stream:
```ts
let interval: NodeJS.Timeout;
const stream = new ReadableStream({
start(controller) {
controller.enqueue("");
interval = setInterval(() => {
controller.enqueue("- " + Math.random() + "
");
}, 100);
setTimeout(() => {
clearInterval(interval);
controller.close();
}, 1000);
},
cancel() {
clearInterval(interval);
},
});
```
## Send a Stream
```ts
import { H3 } from "h3";
export const app = new H3();
app.use((event) => {
// Set to response header to tell to the client that we are sending a stream.
event.res.headers.set("Content-Type", "text/html");
event.res.headers.set("Cache-Control", "no-cache");
event.res.headers.set("Transfer-Encoding", "chunked");
let interval: NodeJS.Timeout;
const stream = new ReadableStream({
start(controller) {
controller.enqueue("");
interval = setInterval(() => {
controller.enqueue("- " + Math.random() + "
");
}, 100);
setTimeout(() => {
clearInterval(interval);
controller.close();
}, 1000);
},
cancel() {
clearInterval(interval);
},
});
return stream;
});
```
Open your browser to http://localhost:3000 and you should see a list of random numbers appearing every 100 milliseconds.
Magic! 🎉
---
# Validate Data
> Ensure that your data are valid and safe before processing them.
When you receive data on your server, you must validate them. By validate, we mean that the shape of the received data must match the expected shape. It's important because you can't trust the data coming from unknown sources, like a user or an external API.
> [!WARNING]
> Do not use type generics as a validation. Providing an interface to a utility like `readBody` is not a validation. You must validate the data before using it.
## Utilities for Validation
H3 provide some utilities to help you to handle data validation. You will be able to validate:
- query with `getValidatedQuery`
- params with `getValidatedRouterParams`.
- body with `readValidatedBody`
H3 doesn't provide any validation library but it does support schemas coming from a **Standard-Schema** compatible one, like: [Zod](https://zod.dev), [Valibot](https://valibot.dev), [ArkType](https://arktype.io/), etc... (for all compatible libraries please check [their official repository](https://github.com/standard-schema/standard-schema)). If you want to use a validation library that is not compatible with Standard-Schema, you can still use it, but you will have to use parsing functions provided by the library itself (refer to the [Safe Parsing](#safe-parsing) section below).
> [!WARNING]
> H3 is runtime agnostic. This means that you can use it in [any runtime](/guide). But some validation libraries are not compatible with all runtimes.
Let's see how to validate data with [Zod](https://zod.dev) and [Valibot](https://valibot.dev).
### Validate Params
You can use `getValidatedRouterParams` to validate params and get the result, as a replacement of `getRouterParams`:
```js
import { getValidatedRouterParams } from "h3";
import * as z from "zod";
import * as v from "valibot";
// Example with Zod
const contentSchema = z.object({
topic: z.string().min(1),
uuid: z.string().uuid(),
});
// Example with Valibot
const contentSchema = v.object({
topic: v.pipe(v.string(), v.nonEmpty()),
uuid: v.pipe(v.string(), v.uuid()),
});
app.all(
// You must use a router to use params
"/content/:topic/:uuid",
async (event) => {
const params = await getValidatedRouterParams(event, contentSchema);
return `You are looking for content with topic "${params.topic}" and uuid "${params.uuid}".`;
},
);
```
If you send a valid request like `/content/posts/123e4567-e89b-12d3-a456-426614174000` to this event handler, you will get a response like this:
```txt
You are looking for content with topic "posts" and uuid "123e4567-e89b-12d3-a456-426614174000".
```
If you send an invalid request and the validation fails, H3 will throw a `400 Validation Error` error. In the data of the error, you will find the validation errors you can use on your client to display a nice error message to your user.
### Validate Query
You can use `getValidatedQuery` to validate query and get the result, as a replacement of `getQuery`:
```js
import { getValidatedQuery } from "h3";
import * as z from "zod";
import * as v from "valibot";
// Example with Zod
const stringToNumber = z.string().regex(/^\d+$/, "Must be a number string").transform(Number);
const paginationSchema = z.object({
page: stringToNumber.optional().default(1),
size: stringToNumber.optional().default(10),
});
// Example with Valibot
const stringToNumber = v.pipe(
v.string(),
v.regex(/^\d+$/, "Must be a number string"),
v.transform(Number),
);
const paginationSchema = v.object({
page: v.optional(stringToNumber, 1),
size: v.optional(stringToNumber, 10),
});
app.use(async (event) => {
const query = await getValidatedQuery(event, paginationSchema);
return `You are on page ${query.page} with ${query.size} items per page.`;
});
```
As you may have noticed, compared to the `getValidatedRouterParams` example, we can leverage validation libraries to transform the incoming data. In this case, we transform the string representation of a number into a real number, which is useful for things like content pagination.
If you send a valid request like `/?page=2&size=20` to this event handler, you will get a response like this:
```txt
You are on page 2 with 20 items per page.
```
If you send an invalid request and the validation fails, H3 will throw a `400 Validation Error` error. In the data of the error, you will find the validation errors you can use on your client to display a nice error message to your user.
### Validate Body
You can use `readValidatedBody` to validate body and get the result, as a replacement of `readBody`:
```js
import { readValidatedBody } from "h3";
import { z } from "zod";
import * as v from "valibot";
// Example with Zod
const userSchema = z.object({
name: z.string().min(3).max(20),
age: z.number({ coerce: true }).positive().int(),
});
// Example with Valibot
const userSchema = v.object({
name: v.pipe(v.string(), v.minLength(3), v.maxLength(20)),
age: v.pipe(v.number(), v.integer(), v.minValue(0)),
});
app.use(async (event) => {
const body = await readValidatedBody(event, userSchema);
return `Hello ${body.name}! You are ${body.age} years old.`;
});
```
If you send a valid POST request with a JSON body like this:
```json
{
"name": "John",
"age": 42
}
```
You will get a response like this:
```txt
Hello John! You are 42 years old.
```
If you send an invalid request and the validation fails, H3 will throw a `400 Validation Error` error. In the data of the error, you will find the validation errors you can use on your client to display a nice error message to your user.
## Safe Parsing
By default if a schema is directly provided as e second argument for each validation utility (`getValidatedRouterParams`, `getValidatedQuery`, and `readValidatedBody`) it will throw a `400 Validation Error` error if the validation fails, but in some cases you may want to handle the validation errors yourself. For this you should provide the actual safe validation function as the second argument, depending on the validation library you are using.
Going back to the first example with `getValidatedRouterParams`, for Zod it would look like this:
```ts
import { getValidatedRouterParams } from "h3";
import { z } from "zod/v4";
const contentSchema = z.object({
topic: z.string().min(1),
uuid: z.string().uuid(),
});
app.all("/content/:topic/:uuid", async (event) => {
const params = await getValidatedRouterParams(event, contentSchema.safeParse);
if (!params.success) {
// Handle validation errors
return `Validation failed:\n${z.prettifyError(params.error)}`;
}
return `You are looking for content with topic "${params.data.topic}" and uuid "${params.data.uuid}".`;
});
```
And for Valibot, it would look like this:
```ts
import { getValidatedRouterParams } from "h3";
import * as v from "valibot";
const contentSchema = v.object({
topic: v.pipe(v.string(), v.nonEmpty()),
uuid: v.pipe(v.string(), v.uuid()),
});
app.all("/content/:topic/:uuid", async (event) => {
const params = await getValidatedRouterParams(event, v.safeParser(contentSchema));
if (!params.success) {
// Handle validation errors
return `Validation failed:\n${v.summarize(params.issues)}`;
}
return `You are looking for content with topic "${params.output.topic}" and uuid "${params.output.uuid}".`;
});
```
---
# Migration guide for v1 to v2
H3 version 2 includes some behavior and API changes that you need to consider applying when migrating.
> [!NOTE]
> Currently H3 v2 in beta stage. You can try with [nightly channel](/guide/advanced/nightly).
> [!NOTE]
> This is an undergoing migration guide and might be updated.
> [!TIP]
> H3 has a brand new documentation rewrite. Head to the new [Guide](/guide) section to learn more!
## Latest Node.js and ESM-only
> [!TIP]
> H3 v2 requires Node.js >= 20.11 (latest LTS recommended) .
If your application is currently using CommonJS modules (`require` and `module.exports`), You can still use `require("h3")` thanks to `require(esm)` supported in latest Node.js versions.
You can alternatively use other compatible runtimes [Bun](https://bun.sh/) or [Deno](https://deno.com/).
## Web Standards
> [!TIP]
> H3 v2 is rewritten based on web standard primitives ([`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL), [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers), [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request), and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)).
When using Node.js, H3 uses a compatibility layer ([💥 srvx](https://srvx.h3.dev/guide/node)) and in other runtimes uses native web compatibility APIs.
Access to the native `event.node.{req,res}` is only available when running server in Node.js runtime.
`event.web` is renamed to `event.req` (instance of web [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request)).
## Response Handling
> [!TIP]
> You should always explicitly **return** the response body or **throw** an error.
If you were previously using methods below, you can replace them with `return` statements returning a text, JSON, stream, or web `Response` (h3 smartly detects and handles each):
- `send(event, value)`: Migrate to `return `.
- `sendError(event, )`: Migrate to `throw createError()`.
- `sendStream(event, )`: Migrate to `return `.
- `sendWebResponse(event, )`: Migrate to `return `.
Other send utils that are renamed and need explicit `return`:
- `sendNoContent(event)` / `return null`: Migrate to `return noContent()`.
- `sendIterable(event, )`: Migrate to `return iterable()`.
- `sendProxy(event, target)`: Migrate to `return proxy(event, target)`.
- `handleCors(event)`: Check return value and early `return` if handled(not `false`).
- `serveStatic(event, content)`: Make sure to add `return` before.
- `sendRedirect(event, location, code)`: Migrate to `return redirect(location, code)`.
:read-more{to="/guide/basics/response" title="Sending Response"}
## H3 and Router
> [!TIP]
> Router function is now integrated into the H3 core.
>
Instead of `createApp()` and `createRouter()` you can use [`new H3()`](/guide/api/h3).
Any handler can return a response. If middleware don't return a response, next handlers will be tried and finally make a 404 if neither responses. Router handlers can return or not return any response, in this case, H3 will send a simple 200 with empty content.
:read-more{to="/guide/basics/lifecycle" title="Request Lifecycle"}
H3 migrated to a brand new route-matching engine ([🌳 rou3](https://rou3.h3.dev/)). You might experience slight (but more intuitive) behavior changes for matching patterns.
**Other changes from v1:**
- Middleware added with `app.use("/path", handler)` only matches `/path` (not `/path/foo/bar`). For matching all subpaths like before, it should be updated to `app.use("/path/**", handler)`.
- The `event.path` received in each handler will have a full path without omitting the prefixes. use `withBase(base, handler)` utility to make prefixed app. (example: `withBase("/api", app.handler)`).
- **`router.add(path, method: Method | Method[]` signature is changed to `router.add(method: Method, path)`**
- `router.use(path, handler)` is deprecated. Use `router.all(path, handler)` instead.
- `app.use(() => handler, { lazy: true })` is no supported anymore. Instead you can use `app.use(defineLazyEventHandler(() => handler), { lazy: true })`.
- `app.use(["/path1", "/path2"], ...)` and `app.use("/path", [handler1, handler2])` are not supported anymore. Instead, use multiple `app.use()` calls.
- `app.resolve(path)` removed.
:read-more{to="/guide/basics/routing" title="Routing"}
:read-more{to="/guide/basics/middleware" title="Middleware"}
## Request Body
> [!TIP]
> Most of request body utilities can now be replaced with native `event.req.*` methods which is based on web [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Response) interface.
`readBody(event)` utility will use [`JSON.parse`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) or [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) for parsing requests with `application/x-www-form-urlencoded` content-type.
- For text: Use [event.req.text()](https://developer.mozilla.org/en-US/docs/Web/API/Request/text).
- For json: Use [event.req.json()](https://developer.mozilla.org/en-US/docs/Web/API/Request/json).
- For formData: Use [event.req.formData()](https://developer.mozilla.org/en-US/docs/Web/API/Request/formData).
- For stream: Use [event.req.body](https://developer.mozilla.org/en-US/docs/Web/API/Request/body).
**Behavior changes:**
- Body utils won't throw an error if the incoming request has no body (or is a `GET` method for example) but instead, return empty values.
- Native `request.json` and `readBody` does not use [unjs/destr](https://destr.unjs.io) anymore. You should always filter and sanitize data coming from user to avoid [prototype-poisoning](https://medium.com/intrinsic-blog/javascript-prototype-poisoning-vulnerabilities-in-the-wild-7bc15347c96).
## Cookie and Headers
> [!TIP]
> H3 now natively uses standard web [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) for all utils.
Header values are always a plain `string` now (no `null` or `undefined` or `number` or `string[]`).
For the [`Set-Cookie`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie) header, you can use [`headers.getSetCookie`](https://developer.mozilla.org/en-US/docs/Web/API/Headers/getSetCookie) that always returns a string array.
## Other Deprecations
H3 v2 deprecated some legacy and aliased utilities.
### App and router utils
- `createApp` / `createRouter`: Migrate to `new H3()`.
### Error utils
- `createError`/`H3Error`: Migrate to `HTTPError`
- `isError`: Migrate to `HTTPError.isError`
### Handler utils
- `eventHandler`/`defineEventHandler`: Migrate to `defineHandler` (you can also directly use a function!).
- `lazyEventHandler`: Migrate to `defineLazyEventHandler`.
- `isEventHandler`: (removed) Any function can be an event handler.
- `useBase`: Migrate to `withBase`.
- `defineRequestMiddleware` and `defineResponseMiddleware` removed.
### Request utils
- `getHeader` / `getRequestHeader`: Migrate to `event.req.headers.get(name)`.
- `getHeaders` / `getRequestHeaders`: Migrate to `Object.fromEntries(event.req.headers.entries())`.
- `getRequestPath`: Migrate to `event.url.pathname`.
- `getMethod`: Migrate to `event.req.method`.
> [!IMPORTANT]
> `getRequestProtocol` (and `getRequestURL`) no longer trust the `x-forwarded-proto` header by default. To honor it (only behind a trusted reverse proxy or CDN), opt in with `{ xForwardedProto: true }`. This matches the existing opt-in behavior of `getRequestHost` (`xForwardedHost`) and `getRequestIP` (`xForwardedFor`).
> [!NOTE]
> The following `H3Event` properties are deprecated in v2 and might be removed in a future version:
>
> - `event.path` → use `event.url.pathname + event.url.search`
> - `event.method` → use `event.req.method`
> - `event.headers` → use `event.req.headers`
> - `event.node` → use `event.runtime.node`
### Response utils
- `getResponseHeader` / `getResponseHeaders`: Migrate to `event.res.headers.get(name)`
- `setHeader` / `setResponseHeader` / `setHeaders` / `setResponseHeaders`: Migrate to `event.res.headers.set(name, value)`.
- `appendHeader` / `appendResponseHeader` / `appendResponseHeaders`: Migrate to `event.res.headers.append(name, value)`.
- `removeResponseHeader` / `clearResponseHeaders`: Migrate to `event.res.headers.delete(name)`
- `appendHeaders`: Migrate to `appendResponseHeaders`.
- `defaultContentType`: Migrate to `event.res.headers.set("content-type", type)`
- `getResponseStatus` / `getResponseStatusText` / `setResponseStatus`: Use `event.res.status` and `event.res.statusText`.
### Node.js utils
- `defineNodeListener`: Migrate to `defineNodeHandler`.
- `fromNodeMiddleware`: Migrate to `fromNodeHandler`.
- `toNodeListener`: Migrate to `toNodeHandler`.
- `createEvent`: (removed): Use Node.js adapter (`toNodeHandler(app)`).
- `fromNodeRequest`: (removed): Use Node.js adapter (`toNodeHandler(app)`).
- `promisifyNodeListener` (removed).
- `callNodeListener`: (removed).
### Web Utils
- `fromPlainHandler`: (removed) Migrate to Web API.
- `toPlainHandler`: (removed) Migrate to Web API.
- `fromPlainRequest` (removed) Migrate to Web API or use `mockEvent` util for testing.
- `callWithPlainRequest` (removed) Migrate to Web API.
- `fromWebRequest`: (removed) Migrate to Web API.
- `callWithWebRequest`: (removed).
### Body Utils
- `readRawBody`: Migrate to `event.req.text()` or `event.req.arrayBuffer()`.
- `getBodyStream` / `getRequestWebStream`: Migrate to `event.req.body`.
- `readFormData` / `readMultipartFormData` / `readFormDataBody`: Migrate to `event.req.formData()`.
### Other Utils
- `createEventStream`: Migrate to `new EventStream(event)`.
- `isStream`: Migrate to `instanceof ReadableStream`.
- `isWebResponse`: Migrate to `instanceof Response`.
- `splitCookiesString`: Use `splitSetCookieString` from [cookie-es](https://github.com/unjs/cookie-es).
- `MIMES`: (removed).
### Type Exports
> [!NOTE]
> There might be more type changes.
- `App`: Migrate to `H3`.
- `AppOptions`: Migrate to `H3Config`.
- `_RequestMiddleware`: Migrate to `RequestMiddleware`.
- `_ResponseMiddleware`: Migrate to `ResponseMiddleware`.
- `NodeListener`: Migrate to `NodeHandler`.
- `TypedHeaders`: Migrate to `RequestHeaders` and `ResponseHeaders`.
- `HTTPHeaderName`: Migrate to `RequestHeaderName` and `ResponseHeaderName`.
- `H3Headers`: Migrate to native `Headers`.
- `H3Response`: Migrate to native `Response`.
- `MultiPartData`: Migrate to native `FormData`.
- `RouteNode`: Migrate to `RouterEntry`.
`CreateRouterOptions`: Migrate to `RouterOptions`.
Removed type exports: `WebEventContext`, `NodeEventContext`, `NodePromisifiedHandler`, `AppUse`, `Stack`, `InputLayer`, `InputStack`, `Layer`, `Matcher`, `PlainHandler`, `PlainRequest`, `PlainResponse`, `WebHandler`.
---
# Blog
H3 release highlights.
---
# H3 1.8 - Towards the Edge of the Web
> New H3 release with web and plain adapters, web streams support, object syntax event handlers, typed event handler requests and more!
> H3 is a versatile H(TTP) framework written in TypeScript that powers both [Nitro](https://nitro.unjs.io/) and [Nuxt](https://nuxt.com/) today.
[Almost two years ago](https://github.com/unjs/h3/tree/cbc8909b2003d6d5df694ab7a36aa067cc990c74), we made H3 with the ambition to become the smallest HTTP framework for [Nuxt 3](https://nuxt.com/), ensuring compatibility with [Node.js](https://nodejs.org/en) and providing an elegant developer experience. It also aimed to have a futuristic design, being adaptable to Edge and Web Worker runtimes, a concept that was relatively new at the time.
During the same period, we also developed [unjs/unenv](https://github.com/unjs/unenv/tree/main), a thin layer that enabled the utilization of Node.js libraries and HTTP middleware for Edge-compatible runtimes without the need for Node.js. This innovation played a pivotal role in enabling us to harness the power of the NPM and Node.js ecosystem without starting everything from scratch for web compatibility. The synergistic combination of H3 and unenv culminated in making [Nitro](https://nitro.unjs.io) one of the pioneering web frameworks fully compatible with Edge runtimes.
This latest release takes H3 even closer to offering native Web API compatibility right out of the box.
> 🚀 This release is immediately available for all ecosystem packages including [Nitro](https://nitro.unjs.io/) and [Nuxt 3](https://nuxt.com/). Please remember to refresh your `lockfile` and `node_modules` to receive the updates.
## Web and Plain Adapters
We have introduced a new built-in adapter with a [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)-compatible signature, with [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) as input and [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) as the return value.
What this signifies is that you can now seamlessly deploy your H3 applications on runtimes such as [Cloudflare Workers](https://workers.cloudflare.com/), [Deno Deploy](https://deno.com/deploy), [Bun](https://bun.sh/), and [Lagon](https://lagon.app/).
For practical examples and a demo, check out the [h3-on-edge](https://github.com/pi0/h3-on-edge) repository.
```ts
// import { createApp, eventHandler, toWebHandler } from 'h3'
import { createApp, eventHandler, toWebHandler } from "https://esm.sh/h3@1.8.0";
const app = createApp();
app.use(
"/",
eventHandler((event) => "H3 works on edge!"),
);
const webHandler = toWebHandler(app); // (Request) => Promise
```
In addition to web handlers, we've also introduced a new plain adapter format using the `toPlainHandler(app)` syntax. This facilitates the seamless integration of H3 with any serverless platform using plain input and response objects.
All of these became possible due to the implementation of new streaming capabilities and [unjs/unenv](https://unenv.unjs.io), which provides a lightweight Node.js compatibility layer. Previously, this level of integration was only possible through [Nitro presets](https://nitro.unjs.io/deploy).
Furthermore, we've introduced a set of new web helpers:
- `toWebRequest(event)`: Convert a H3 event object into a web [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request).
- `getRequestWebStream(event)`: Retrieve a readable stream from the current H3 event request.
- `fromPlainHandler(plainHandler)`: Convert a plain object handler into an H3-compatible event handler.
- `fromWebHandler(webHandler)`: Convert a Web Request/Response handler into an H3-compatible event handler.
## Web Streams Support
H3 now supports native [Readable Stream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) response support. This inherently brings compatibility with libraries like [Vercel/AI](https://github.com/vercel/ai), which rely on streaming responses ([demo](https://github.com/Hebilicious/nuxt-openai-vercel-edge-demo)).
Leveraging this functionality is straightforward—simply return a [Readable Stream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) or [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) object from your event handlers.
```ts
export default defineHandler((event) => {
setResponseHeader(event, "Content-Type", "text/html");
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for (const token of "Streaming is so cool with H3!".split(" ")) {
controller.enqueue(encoder.encode(token));
await new Promise((resolve) => {
setTimeout(resolve, 300);
});
}
},
});
return stream;
});
```
For more advanced scenarios, you might choose to utilize the `sendStream(event, stream)` and `sendWebResponse(event, stream)` utilities instead of directly returning the stream.
## Object Syntax Event Handlers
H3 introduces support for defining event handlers using an Object syntax. With this approach, you can define hooks that run before or after each handler, such as authentication or compression middleware.
```ts
const auth = defineRequestMiddleware((event) => {
event.context.auth = { name: "admin" };
});
const compression = defineResponseMiddleware((event) => {
// Example: https://stackblitz.com/edit/github-mb6bz3
});
export default eventHandler({
onRequest: [auth],
onResponse: [compression],
async handler(event) {
return `Hello ${event.context.auth?.name || "Guest"}`;
},
});
```
## Typed Event Handler Requests
H3 now supports defining event types using new generic type support.
When you define types, request utilities will be aware of the event input types. This enhancenment also allows us to enhance type safety for `$fetch` handlers in upstream frameworks like [Nitro](https://nitro.unjs.io/) and [Nuxt](https://nuxt.com/).
```ts
export default eventHandler<{ body: { name: string }; query: { id: string } }>(async (event) => {
const query = getQuery(event); // Query is typed as { id: string }
const body = await readBody(event); // Body is typed as { name: string }
});
```
## Runtime + Type-Safe Request Utils
Two new utility functions, `getValidatedQuery(event, validator)` and `readValidatedBody(event, validator)`, facilitate integration with schema validators such as [zod](https://zod.dev/) for both runtime and type safety.
```ts
import { z } from "zod";
const userSchema = z.object({
name: z.string().default("Guest"),
email: z.string().email(),
});
export default defineHandler(async (event) => {
const result = await readValidatedBody(event, (body) => userSchema.safeParse(body)); // or `.parse` to directly throw an error
if (!result.success) throw result.error.issues;
// User object is validated and typed!
return result.data;
});
```
## Additional Utilities
We've introduced several other utilities to further enhance the web app development experience:
- `getRequestIP(event, { xForwardedFor? })`: Retrieve the incoming request IP.
- `readFormData(event)`: Read the request body into [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData).
- `clearResponseHeaders(event)`: Clear all response headers.
- `removeResponseHeader(event, name)`: Remove a specific response header.
- `serveStatic(event, options)`: Platform-agnostic static asset server. Check out the [listhen source](https://github.com/unjs/listhen/blob/af6ea3af3fec4289c00b0ba589ca6f63c6a5dbbd/src/server/dev.ts#L66) for an example of usage with Node.js.
## Effortless TypeScript Development with HMR
We've also released an updated version of [unjs/listhen](https://listhen.unjs.io) that seamlessly integrates with H3 apps.
All you need to do is create an `index.ts` file:
```ts
import { createApp, eventHandler } from "h3";
export const app = createApp();
app.use("/", () => "Hello world!");
```
Run `npx listhen@latest -w ./index.ts` to initiate a development server with TypeScript support, Hot Module Replacement (HMR), and static asset server.
[Online Playground](https://stackblitz.com/github/unjs/h3/tree/main/playground?startScript=dev)
{withoutBorder}
## Full Changelog
For a comprehensive list of changes, refer to the [release notes](https://github.com/unjs/h3/releases/tag/v1.8.0).
---
# H3 v2 beta
> ⚡ H3 v2 beta is here — fully rewritten on web standards, backward-compatible, and faster than ever!
::read-more{to="/guide"}
Visit the new [**H3 Guide**](/guide) to get started quickly.
::
H3 started in late 2020, during the rise of edge workers. With H3 + [unjs/unenv](https://github.com/unjs/unenv), we could run [Nitro](https://nitro.build) deployments in worker environments with Node.js compatibility, best of both worlds! Since [v1.8](/blog/v1.8), H3 has improved its support for web standards.
But still, H3 was primarily based on Node.js APIs with a compatibility layer for web standards. Logical choice at the time, given Node.js's popularity amongst JavaScript server runtimes.
Thanks to evolving web standards by initiatives like [WinterTC](https://wintertc.org/) and runtime support in [Deno](https://deno.com/), [Bun](https://bun.sh/), and the latest [Node.js](https://nodejs.org/en), ecosystem is ready to embrace web standards first class for server development. Benefits include:
- Cross-runtime interoperability (Node.js, Deno, Bun, Workers, etc.)
- Cross-framework compatibility (H3, Hono, Elysia, etc.)
- Cross-environment compatibility (shared and familiar code between frontend and backend)
- Leverage more of runtime native primitives like (Request, URL, Headers, etc.)
- Easier API testing
## 💥 srvx: Universal Web Server API
A major challenge was that Node.js lacks built-in support for web-standard HTTP servers. For `node:http` compatibility, an adapter is needed to bridge Node.js `IncomingMessage` to web `Request`, and to handle web `Response` via Node.js `ServerResponse`. We have implemented a [compatibility layer](https://srvx.h3.dev/guide/node) that bridges interfaces and achieves **up to 96.98% of native `node:http` performance** (see [benchmarks](https://github.com/h3js/srvx/tree/main/test/bench-node)).
Runtimes such as [Deno](https://deno.com/), [Bun](https://bun.sh/), and Edge Workers pioneered the adoption of web standards for servers, but they did not agree on the same interface due to lack of enough specs. So how do you access the client IP address and additional context? How do you set the server port and TLS options? How do you handle WebSocket upgrades? Each runtime created its own API.
We have created [💥 srvx](https://srvx.h3.dev): A unified layer that works everywhere exactly the same. Compatible with Deno, Bun, Node.js, Service Workers, Edge Workers.
Example
```js
// Dynamic adapter will be used based export conditions of each runtime
import { serve } from "srvx";
serve({
port: 3000,
// tls: { cert: "server.crt", key: "server.key" }
fetch(req) {
// Server Extensions: req.ip, req.waitUntil(), req.runtime?.{bun,deno,node,cloudflare,...}
return new Response("👋 Hello there!");
},
});
```
> [!TIP]
> With [💥 srvx](https://srvx.h3.dev) unifying runtime differences, H3 can remain simpler, focusing exclusively on web standard APIs.
## ⚡ H3: Tiny Server Composer 🎶
We worked hard to minimize and simplify H3’s scope.
- 🪶 Optimized for performances, [lighter](#lighter-than-a-feather) than a feather.
- 👌 Intuitive [typed handlers](/guide/basics/handler), [responses](/guide/basics/response) and [errors](/guide/basics/error).
- 🧩 Reusable [middleware](/guide/basics/middleware) and [plugins](/guide/advanced/plugins).
- 🌳 Fast [routing](/guide/basics/routing).
- ➕ Built-in [utilities](/utils).
- ❤️ Maximum [compatibility](/guide/api/h3#h3mount) based on web standards.
```js
import { H3, serve } from "h3";
const app = new H3().get("/", () => "⚡️ Tadaa!");
serve(app, { port: 3000 });
```
## 🪶 Lighter Than a Feather
We approached benchmarking with a new method that focuses on measuring the overhead introduced by the framework itself, rather than the network layer. Our goal is to optimize all relevant measurements together, making the numbers as close as possible to a baseline where no framework is added or used. This method allowed H3 to achieve optimized latency improvements per request and a dramatically smaller core bundle size.
| Measurement | H3 v1 | 🚀 H3 v2 |
| ---------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Request Handling | Node: 36 µs
Bun: 27 µs
Deno: 7 ms | Node: **7 µs** (**5x faster**)
Bun: **3 µs** (**9x faster**)
Deno: **1.2 µs** (**156x faster**) |
| Bundle Size | min: 101 kB
min+gzip: 39.6 kB | min: **9,1 kB** (**91% smaller**)
min+gzip: **3.6 kB** (**90% smaller**)
min: **5.2 kB** / min+gzip: **2.1 kB** ([fetchable](/guide/basics/handler#handler-fetch) handlers) |
> [!TIP]
> H3 v2 performance is nearly identical to plain `fetch` handler with `new URL(req.url).pathname` for routing. In other words, you get the benefits of H3 with nearly zero performance cost!
> [!NOTE]
> Benchmarks apply to the H3 core using the Web Standard target and do not include adapters. They are primarily intended for internal optimization purposes. See the [benchmark](https://github.com/h3js/h3/tree/main/test/bench) for details and [srvx benchmarks](https://github.com/h3js/srvx/tree/main/test/bench-node) for Node.js adapter performances.
## ✅ Typed Web Standards
H3 adopts web standard APIs such as [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request), [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response), [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL), and [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers), without introducing new conventions on top of the standards.
We have launched a new initiative to strongly type Web APIs: [✅ fetchdts](https://github.com/unjs/fetchdts). Integrated into the H3, now we combine the best of both worlds—standards and the convenience of types.
```js
import { defineHandler } from "h3";
const handler = defineHandler(async (event) => {
// URL Parsing
const { pathname, searchParams } = event.url;
// Access to request headers (try auto-completion in editor!)
const accept = event.req.headers.get("Accept");
// Read body
const bodyStream = await event.req.body;
const bodyText = await event.req.text();
const bodyJSON = await event.req.json();
const bodyFormData = await event.req.formData();
// Access to runtime specific context
const { deno, bun, node } = event.req.runtime;
// Prepare response (h3 does this smartly)
event.res.headers.set("Content-Type", "application/json");
return { hello: "web" };
});
```
Now go ahead and call handler [`.fetch`](/guide/basics/handler#handler-fetch):
```js
const response = await handler.fetch("/");
// 🧙 Typed response: { hello: string; }
const json = await response.json();
```
> [!TIP]
> You can directly use event handlers as a standalone, even smaller web handlers without h3 core!
## 🧩 Middleware and Plugins
H3 now offers an ergonomic, composable way to chain middleware using `next()` function (inspired by [Hono middleware](https://hono.dev/docs/guides/middleware) 💛).
Additionally, we have introduced a simple yet powerful pattern to extend H3 apps using reusable [plugins](/guide/advanced/plugins).
```js [middleware]
import { H3 } from "h3";
const app = new H3().use(async (event, next) => {
// ... before response ...
const body = await next();
// ... after response ...
event.res.headers.append("x-middleware", "works");
event.waitUntil(sendMetrics(event));
return body;
});
```
```js [basic auth]
import { defineHandler, basicAuth } from "h3";
export default defineHandler({
middleware: [basicAuth({ password: "test" })],
handler: (event) => `Hello ${event.context.basicAuth?.username}!`,
});
```
```js [onRequest]
import { H3, onRequest } from "h3";
const app = new H3().use(
onRequest((event) => {
console.log(`Request: [${event.req.method}] ${event.url.pathname}`);
}),
);
```
```js [onResponse]
import { H3, onResponse } from "h3";
const app = new H3().use(
onResponse((response, event) => {
console.log(`Response: [${event.req.method}] ${event.url.pathname}`, body);
}),
);
```
```js [onError]
import { H3, onError } from "h3";
const app = new H3().use(
onError((error, event) => {
console.error(`[${event.req.method}] ${event.url.pathname} !! ${error.message}`);
}),
);
```
```js [plugins]
import { H3, serve, definePlugin } from "h3";
const logger = definePlugin((h3, _options) => {
if (h3.config.debug) {
h3.use((req) => {
console.log(`[${req.method}] ${req.url}`);
});
}
});
const app = new H3({ debug: true }).register(logger()).all("/**", () => "Hello!");
```
> [!NOTE]
> Accepting `next` callback is optional. Middleware can be written like v1 without returning a response.
## ⬆️ Migration from Version 1
We've tried to minimize breaking changes. Most of utilities preserved backward compatibility.
::read-more{to="/migration"}
Check out [Migration Guide](/migration).
::
## 🙌 Unified H(TTP) Server Tools for Everyone
H3 and related projects moved to a dedicated [github org](https://github.com/h3js) and new [h3.dev](https://h3.dev) domain (thanks to the donation from [syntax.fm](https://syntax.fm/) and other [sponsors](/#sponsors) 💛).
Under the H3 umbrella, we maintain several key components for universal JavaScript servers.
All fully open and usable with **or without** H3, and with any JavaScript runtime.
- [⚡️ h3](https://github.com/h3js/h3): Minimal HTTP Framework.
- [🌳 rou3](https://github.com/h3js/rou3): Lightweight JavaScript Router.
- [💥 srvx](https://srvx.h3.dev): Universal Web-based Server API.
- [🔌 crossws](https://crossws.h3.dev): Cross-platform WebSocket support.
## ❤️ Special Thanks
This release would not have been possible without wonderful [contributors](https://github.com/h3js/h3/graphs/contributors), feedback from the [community](https://discord.h3.dev), inspirations from web-standard frameworks including [Hono](https://hono.dev/) and [Elysia](https://elysiajs.com/), and [sponsors](/#sponsors) who made it possible to work on open source.
## 🗺️ Roadmap to v2 (stable)
**Next steps:**
- Gather feedback from community.
- Finalize API updates based on feedbacks.
- Ensure ecosystem compatibility and upgrade for [Nitro](https://nitro.build) v3.
::callout{to="https://discord.h3.dev"}
Join our [Discord](https://discord.h3.dev) to share your experience and feedback!
::
::read-more{to="/guide"}
Visit the new [**H3 Guide**](/guide) to get started quickly.
::