# Getting Started
::important
You are currently reading H3 v2 docs. See [v1.h3.dev](https://v1.h3.dev/){rel=""nofollow""} 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){rel=""nofollow""}, [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response){rel=""nofollow""}, [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL){rel=""nofollow""}, and [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers){rel=""nofollow""}. You can integrate H3 with any compatible runtime or [mount](https://h3.dev/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](https://h3.dev/guide/api/h3) and then import built-in, tree-shakable [utilities](https://h3.dev/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](https://h3.dev/guide/api/h3event) context.
:read-more{title="built-in H3 utilities" to="https://h3.dev/utils"}
## Quick Start
::tip
You try H3 online [on ⚡️ Stackblitz ](https://stackblitz.com/github/h3js/h3/tree/main/playground?file=server.mjs){rel=""nofollow""}.
::
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](https://h3.dev/guide/api/h3) app instance using `new H3()`:
```ts
const app = new H3();
```
[H3](https://h3.dev/guide/api/h3) is a tiny class capable of [matching routes](https://h3.dev/guide/basics/routing), [generating responses](https://h3.dev/guide/basics/response) and calling [middleware](https://h3.dev/guide/basics/middleware) and [global hooks](https://h3.dev/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="https://h3.dev/guide/basics/routing"}
We simply returned an object. H3 automatically [converts](https://h3.dev/guide/basics/response#response-types) values into web responses.
:read-more{title="Sending Response" to="https://h3.dev/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/){rel=""nofollow""}, a runtime-agnostic universal server listener based on web standards that works seamlessly with [Deno](https://deno.com/){rel=""nofollow""}, [Node.js](https://nodejs.org/){rel=""nofollow""} and [Bun](https://bun.sh/){rel=""nofollow""}.
::
We also have [`app.fetch`](https://h3.dev/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{title="H3.fetch" to="https://h3.dev/guide/api/h3#h3fetch"}
```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
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){rel=""nofollow""}, server fetch handler receives a [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request){rel=""nofollow""} object.
::mermaid
---
code: >
%%{init: {'theme':'neutral'}}%%
flowchart LR
A1["fetch(request)"] --> A2["server.fetch(request)"]
click A2 "/guide/api/h3#h3fetch"
---
::
::tip
[💥 Srvx](https://srvx.h3.dev){rel=""nofollow""} provides unified `server.fetch` interface and adds [Node.js compatibility](https://srvx.h3.dev/guide/node){rel=""nofollow""}.
::
## 2. Accept Request
H3 Initializes an [`H3Event`](https://h3.dev/guide/api/h3event) instance from incoming request, calls [`onRequest`](https://h3.dev/guide/api/h3#global-hooks) global hook and finally [`H3.handler`](https://h3.dev/guide/api/h3#h3handler) with the initialized event.
::mermaid
---
code: >
%%{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](https://h3.dev/guide/basics/routing) based on `request.url` and `request.method`, calls global [middleware](https://h3.dev/guide/basics/middleware) and finally matched route handler function with event.
::mermaid
---
code: |+
%%{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](https://h3.dev/guide/basics/response#response-types) returned value and [prepared headers](https://h3.dev/guide/basics/response#preparing-response) into a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response){rel=""nofollow""}, calls [`onResponse`](https://h3.dev/guide/api/h3#global-hooks) global hook and finally returns response back to the server fetch handler.
::mermaid
---
code: >
%%{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
## Adding Routes
You can register route [handlers](https://h3.dev/guide/basics/handler) to [H3 instance](https://h3.dev/guide/api/h3) using [`H3.on`](https://h3.dev/guide/api/h3#h3on), [`H3.[method]`](https://h3.dev/guide/api/h3#h3method), or [`H3.all`](https://h3.dev/guide/api/h3#h3all).
::tip
Router is powered by [🌳 Rou3](https://github.com/h3js/rou3){rel=""nofollow""}, 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]`](https://h3.dev/guide/api/h3#h3method)
```js
app.get("/hello", () => "Hello world!");
```
- Using [`H3.on`](https://h3.dev/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`](https://h3.dev/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!`);
```
## 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="https://h3.dev/guide/basics/handler#meta"}
It is also possible to add route meta when defining them using `defineHandler` object syntax.
::
# 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{title="Request Lifecycle" to="https://h3.dev/guide/basics/lifecycle#\_3-dispatch-request"}
You can register global middleware to [app instance](https://h3.dev/guide/api/h3) using the [`H3.use`](https://h3.dev/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
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`](https://h3.dev/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](https://h3.dev/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{title="Response Handling" to="https://h3.dev/guide/basics/response"}
:read-more{to="https://h3.dev/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="https://h3.dev/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](https://h3.dev/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:
::CodeGroup
```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){rel=""nofollow""} => [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response){rel=""nofollow""} signuture can be converted into H3 event handlers using `fromWebHandler` utility or [H3.mount](https://h3.dev/guide/api/h3#h3mount).
```js
import { H3, fromWebHandler } from "h3";
export const app = new H3();
const webHandler = (request) => new Response("👋 Hello!");
// Using fromWebHandler utiliy
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
Values returned from [Event Handlers](https://h3.dev/guide/basics/handler) are automatically converted to a web [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response){rel=""nofollow""} 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){rel=""nofollow""} or from an [async function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function){rel=""nofollow""}, 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{title="Error Handling" to="https://h3.dev/guide/basics/error"}
## Preparing Response
Before returning a response in main handler, you can prepare response headers and status using [`event.res`](https://h3.dev/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){rel=""nofollow""} value is returned, prepared status is discarded and headers will be merged/overriden. 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){rel=""nofollow""}.
### JSON Serializable Value
Returning a [JSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON){rel=""nofollow""} 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){rel=""nofollow""} 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){rel=""nofollow""} 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.
```js
import { html } from "h3";
app.get("/", () => html("hello world
"));
```
### `Response`
Returning a web [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response){rel=""nofollow""}, sends-it as final reponse.
**Example:**
```ts
app.get("/", (event) => new Response("Hello, world!", { headers: { "x-powered-by": "H3" } }));
```
::important
When sending a `Response`, any [prepared headers](https://h3.dev/#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`.
::
### `ReadableStream` or `Readable`
Returning a [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream){rel=""nofollow""} or Node.js [`Readable`](https://nodejs.org/api/stream.html#readable-streams){rel=""nofollow""} 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){rel=""nofollow""}, [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array){rel=""nofollow""} or node [Buffer](https://nodejs.org/api/buffer.html#buffer){rel=""nofollow""}.
`content-length` header will be automatically set.
### `Blob`
Send a [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob){rel=""nofollow""} 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){rel=""nofollow""} 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){rel=""nofollow""} 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{title="Error Handling" to="https://h3.dev/guide/basics/error"}
### `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){rel=""nofollow""} 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
H3 captures all possible errors during [request lifecycle](https://h3.dev/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 pbject
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](https://h3.dev/#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](https://h3.dev/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](https://h3.dev/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`](https://h3.dev/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](https://h3.dev/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
Typically, H3 projects consist of several [Event Handlers](https://h3.dev/guide/basics/handler) defined in one or multiple files (or even [lazy loaded](https://h3.dev/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`](https://h3.dev/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/){rel=""nofollow""} or [Elysia](https://elysiajs.com/){rel=""nofollow""} 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){rel=""nofollow""} or [Elysia](https://elysiajs.com/patterns/mount#mount-1){rel=""nofollow""}.
::
# H3
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){rel=""nofollow""}-compatible function allowing to fetch app routes.
- Input can be a relative path, [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL){rel=""nofollow""}, or [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request){rel=""nofollow""}.
- Returned value is a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response){rel=""nofollow""} 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{title="Routing" to="https://h3.dev/guide/basics/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](https://h3.dev/guide/basics/middleware).
```js
const app = new H3()
.use((event) => {
console.log(`request: ${event.req.url}`);
})
.all("/", () => "OK");
```
:read-more{title="Middleware" to="https://h3.dev/guide/basics/middleware"}
### `H3.register`
Register a H3 plugin to extend app.
:read-more{title="Plugins" to="https://h3.dev/guide/advanced/plugins"}
### `H3.handler`
An H3 [event handler](https://h3.dev/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{title="Nested Apps" to="https://h3.dev/guide/basics/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.
- `plugins`: (see [plugins](https://h3.dev/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](https://h3.dev/guide/basics/middleware) for more flexibility.
::
## `H3` Properties
### `H3.config`
Global H3 instance config.
# H3Event
With each HTTP request, H3 internally creates an `H3Event` object and passes it though event handlers until sending the response.
:read-more{title="Request Lifecycle" to="https://h3.dev/guide/basics/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.
::CodeGroup
```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,
}),
});
}
```
::
## `H3Event` Properties
### `H3Event.app?`
Access to the H3 [application instance](https://h3.dev/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){rel=""nofollow""} with additional runtime addons (see [srvx docs](https://srvx.h3.dev/guide/handler#extended-request-context){rel=""nofollow""}).
```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){rel=""nofollow""}.
### `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{title="Preparing Response" to="https://h3.dev/guide/basics/response#preparing-response"}
# Plugins
## Register Plugins
Plugins can be registered either when creating a new [H3 instance](https://h3.dev/guide/api/h3) or by using [H3.register](https://h3.dev/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](https://h3.dev/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
You can add cross platform WebSocket support to H3 servers using [🔌 CrossWS](https://crossws.h3.dev/){rel=""nofollow""}.
::important
Built-in support of WebSockets in h3 version is WIP.
::
## 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 and provide a `resolve` function to resolve the correct hooks from the route.
```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({ resolve: async (req) => (await app.fetch(req)).crossws })],
});
```
**Full example:**
```js [websocket.mjs]
import { H3, serve, defineWebSocketHandler } from "h3";
import { plugin as ws } from "crossws/server";
export const app = new H3();
const demoURL =
"https://raw.githubusercontent.com/h3js/crossws/refs/heads/main/playground/public/index.html";
app.get("/", () =>
fetch(demoURL).then(
(res) => new Response(res.body, { headers: { "Content-Type": "text/html" } }),
),
);
app.get(
"/_ws",
defineWebSocketHandler({
// upgrade(req) {},
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");
},
}),
);
serve(app, {
plugins: [ws({ resolve: async (req) => (await app.fetch(req)).crossws })],
});
```
## 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){rel=""nofollow""}.
H3 has a built-in API to create server-sent events using `createEventStream(event)` utility.
### Example
```js [server-sent-events.mjs]
import { H3, serve, createEventStream } from "h3";
export const app = new H3();
app.get("/", (event) => {
const eventStream = createEventStream(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.send();
});
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](https://h3.dev/guide/api/h3) instance and for every functionality, there is either a built-in utility or you can make yours.
::card-group
:::card
---
icon: material-symbols-light:input
title: Request
to: https://h3.dev/utils/request
---
Utilities for incoming request.
:::
:::card
---
icon: material-symbols-light:output
title: Response
to: https://h3.dev/utils/response
---
Utilities for preparing and sending response.
:::
:::card
---
icon: material-symbols:cookie-outline
title: Cookie
to: https://h3.dev/utils/cookie
---
Cookie utilities.
:::
:::card
---
icon: wpf:key-security
title: Security
to: https://h3.dev/utils/security
---
Security utilities.
:::
:::card
---
icon: arcticons:super-proxy
title: Proxy
to: https://h3.dev/utils/proxy
---
Proxy utilities.
:::
:::card
---
icon: material-symbols:swap-calls
title: MCP
to: https://h3.dev/utils/mcp
---
MCP related utilities.
:::
:::card{icon="ri:more-line" title="More" to="https://h3.dev/utils/more"}
More Utilities.
:::
:::card
---
icon: pixelarticons:github
title: Community
to: https://h3.dev/utils/community
---
Community made utilities.
:::
::
# Request
## Body
### `assertBodySize(event, limit)`
Asserts that request body size is within the specified limit.
If body size exceeds the limit, throws a `413` Request Entity Too Large response error.
**Example:**
```ts
app.get("/", async (event) => {
await assertBodySize(event, 10 * 1024 * 1024); // 10MB
const data = await event.req.formData();
});
```
### `readBody(event)`
Reads request body and tries to parse using JSON.parse or URLSearchParams.
**Example:**
```ts
app.get("/", async (event) => {
const body = await readBody(event);
});
```
### `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),
}),
},
);
});
```
## Cache
### `handleCacheHeaders(event, opts)`
Check request caching headers (`If-Modified-Since`) and add caching headers (Last-Modified, Cache-Control) Note: `public` cache control will be added by default
## 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.
**Example:**
```ts
app.get("/", (event) => {
const query = getQuery(event); // { key: "value", key2: ["value1", "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.
**Example:**
```ts
app.get("/", (event) => {
const host = getRequestHost(event); // "example.com"
});
```
### `getRequestIP(event)`
Try to get the client IP address from the incoming request.
If `xForwardedFor` is `true`, it will use the `x-forwarded-for` header if it exists.
If IP cannot be determined, it will default to `undefined`.
**Example:**
```ts
app.get("/", (event) => {
const ip = getRequestIP(event); // "192.0.2.0"
});
```
### `getRequestProtocol(event, opts: { xForwardedProto? })`
Get the request protocol.
If `x-forwarded-proto` header is set to "https", it will return "https". You can disable this behavior by setting `xForwardedProto` to `false`.
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 `false`, it will not use the `x-forwarded-proto` header.
**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 using `decodeURI`.
**Example:**
```ts
app.get("/", (event) => {
const param = getRouterParam(event, "key");
});
```
### `getRouterParams(event, opts: { decode? })`
Get matched route params.
If `decode` option is `true`, it will decode the matched route params using `decodeURIComponent`.
**Example:**
```ts
app.get("/", (event) => {
const params = getRouterParams(event); // { key: "value" }
});
```
### `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`, it will decode the matched route params using `decodeURI`.
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)`
Create a lightweight request proxy with the base path stripped from the URL pathname.
### `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){rel=""nofollow""}.
If input is a relative URL, it will be normalized into a full path based on headers.
If input is already a Request and no options are provided, it will be returned as-is.
### `getRequestFingerprint(event, opts)`
Get a unique fingerprint for the incoming request.
# Response
## Event Stream
### `createEventStream(event, opts?)`
Initialize an EventStream instance for creating [server sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events){rel=""nofollow""}
**Example:**
```ts
import { createEventStream, sendEventStream } from "h3";
app.get("/sse", (event) => {
const eventStream = createEventStream(event);
// Send a message every second
const interval = setInterval(async () => {
await eventStream.push("Hello world");
}, 1000);
// cleanup the interval and close the stream when the connection is terminated
eventStream.onClosed(async () => {
console.log("closing SSE...");
clearInterval(interval);
await eventStream.close();
});
return eventStream.send();
});
```
## 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: {rel=""nofollow""}
## Serve Static
### `serveStatic(event, options)`
Dynamically serve static assets based on the request path.
## 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.
**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. :br
**Example:**
```ts
app.get("/", () => noContent());
```
### `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.
**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
### `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
## 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.
### `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.
## 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.
# Proxy
### `fetchWithEvent(event, url, init?)`
Make a fetch request with the event's context and headers.
If the `url` starts with `/`, the request is dispatched internally via `event.app.fetch()` (sub-request) and never leaves the process.
**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).
**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).
### `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.
**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.
# MCP
### `defineJsonRpcHandler()`
Creates an H3 event handler that implements the JSON-RPC 2.0 specification.
**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.
**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
## 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 checks whether request body size is within specified limit.
If body size exceeds the limit, throws a `413` Request Entity Too Large response error. If you need custom handling for this case, use `assertBodySize` instead.
### `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.
### `defineWebSocketHandler()`
Define WebSocket event handler.
## Adapters
### `defineNodeHandler(handler)`
### `defineNodeMiddleware(handler)`
### `fromNodeHandler(handler)`
### `fromWebHandler(handler)`
# 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.
:br:br
::tip
💛 PR is more than welcome to list yours.
::
## `apitally`
[Apitally](https://apitally.io/h3){rel=""nofollow""} 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){rel=""nofollow""}.
:read-more{to="https://github.com/apitally/apitally-js"}
## `H3ravel Framework`
[H3ravel Framework](https://h3ravel.toneflix.net){rel=""nofollow""} 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){rel=""nofollow""}.
:read-more{to="https://github.com/h3ravel"}
## `Intlify`
[Intlify](https://intlify.dev/){rel=""nofollow""} 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){rel=""nofollow""}
: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"}
# Examples
::read-more{to="https://github.com/h3js/h3/tree/main/examples"}
Check [`examples/` dir](https://github.com/h3js/h3/tree/main/examples){rel=""nofollow""} for more examples.
::
**Examples:**
- [Cookies](https://h3.dev/examples/handle-cookie)
- [Session](https://h3.dev/examples/handle-session)
- [Static Assets](https://h3.dev/examples/serve-static-assets)
- [Streaming Response](https://h3.dev/examples/stream-response)
- [Validation](https://h3.dev/examples/validate-data)
# Cookies
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){rel=""nofollow""}:
- `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="https://h3.dev/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.
# Sessions
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` initializes or retrieves the current user session.
- `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](https://h3.dev/guide/handler):
```js
import { useSession } from "h3";
app.use(async (event) => {
const session = await useSession(event, {
password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9",
});
// do something...
});
```
::warning
You must provide a password to encrypt the session.
::
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;
});
```
# Static Assets
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 {rel=""nofollow""}. 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
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){rel=""nofollow""} 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 {rel=""nofollow""} and you should see a list of random numbers appearing every 100 milliseconds.
Magic! 🎉
# Validate Data
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){rel=""nofollow""}, [Valibot](https://valibot.dev){rel=""nofollow""}, [ArkType](https://arktype.io/){rel=""nofollow""}, etc... (for all compatible libraries please check [their official repository](https://github.com/standard-schema/standard-schema){rel=""nofollow""}). 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](https://h3.dev/#safe-parsing) section below).
::warning
H3 is runtime agnostic. This means that you can use it in [any runtime](https://h3.dev/adapters). But some validation libraries are not compatible with all runtimes.
::
Let's see how to validate data with [Zod](https://zod.dev){rel=""nofollow""} and [Valibot](https://valibot.dev){rel=""nofollow""}.
### 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](https://h3.dev/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](https://h3.dev/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/){rel=""nofollow""} or [Deno](https://deno.com/){rel=""nofollow""}.
## Web Standards
::tip
H3 v2 is rewritten based on web standard primitives ([`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL){rel=""nofollow""}, [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers){rel=""nofollow""}, [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request){rel=""nofollow""}, and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response){rel=""nofollow""}).
::
When using Node.js, H3 uses a compatibility layer ([💥 srvx](https://srvx.h3.dev/guide/node){rel=""nofollow""}) 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){rel=""nofollow""}).
## 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{title="Sending Response" to="https://h3.dev/guide/basics/response"}
## H3 and Router
::tip
Router function is now integrated into the H3 core.
:br Instead of `createApp()` and `createRouter()` you can use [`new H3()`](https://h3.dev/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{title="Request Lifecycle" to="https://h3.dev/guide/basics/lifecycle"}
H3 migrated to a brand new route-matching engine ([🌳 rou3](https://rou3.h3.dev/){rel=""nofollow""}). 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{title="Routing" to="https://h3.dev/guide/basics/routing"}
:read-more{title="Middleware" to="https://h3.dev/guide/basics/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){rel=""nofollow""} interface.
::
`readBody(event)` utility will use [`JSON.parse`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse){rel=""nofollow""} or [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams){rel=""nofollow""} 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){rel=""nofollow""}.
- For json: Use [event.req.json()](https://developer.mozilla.org/en-US/docs/Web/API/Request/json){rel=""nofollow""}.
- For formData: Use [event.req.formData()](https://developer.mozilla.org/en-US/docs/Web/API/Request/formData){rel=""nofollow""}.
- For stream: Use [event.req.body](https://developer.mozilla.org/en-US/docs/Web/API/Request/body){rel=""nofollow""}.
**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){rel=""nofollow""} 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){rel=""nofollow""}.
## Cookie and Headers
::tip
H3 now natively uses standard web [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers){rel=""nofollow""} 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){rel=""nofollow""} header, you can use [`headers.getSetCookie`](https://developer.mozilla.org/en-US/docs/Web/API/Headers/getSetCookie){rel=""nofollow""} 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`.
::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
- `isStream`: Migrate to `instanceof ReadableStream`.
- `isWebResponse`: Migrate to `instanceof Response`.
- `splitCookiesString`: Use `splitSetCookieString` from [cookie-es](https://github.com/unjs/cookie-es){rel=""nofollow""}.
- `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`.
# H3 1.8 - Towards the Edge of the Web
> H3 is a versatile H(TTP) framework written in TypeScript that powers both [Nitro](https://nitro.unjs.io/){rel=""nofollow""} and [Nuxt](https://nuxt.com/){rel=""nofollow""} today.
[Almost two years ago](https://github.com/unjs/h3/tree/cbc8909b2003d6d5df694ab7a36aa067cc990c74){rel=""nofollow""}, we made H3 with the ambition to become the smallest HTTP framework for [Nuxt 3](https://nuxt.com/){rel=""nofollow""}, ensuring compatibility with [Node.js](https://nodejs.org/en){rel=""nofollow""} 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){rel=""nofollow""}, 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){rel=""nofollow""} 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/){rel=""nofollow""} and [Nuxt 3](https://nuxt.com/){rel=""nofollow""}. 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){rel=""nofollow""}-compatible signature, with [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request){rel=""nofollow""} as input and [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response){rel=""nofollow""} 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/){rel=""nofollow""}, [Deno Deploy](https://deno.com/deploy){rel=""nofollow""}, [Bun](https://bun.sh/){rel=""nofollow""}, and [Lagon](https://lagon.app/){rel=""nofollow""}.
For practical examples and a demo, check out the [h3-on-edge](https://github.com/pi0/h3-on-edge){rel=""nofollow""} 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){rel=""nofollow""}, which provides a lightweight Node.js compatibility layer. Previously, this level of integration was only possible through [Nitro presets](https://nitro.unjs.io/deploy){rel=""nofollow""}.
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){rel=""nofollow""}.
- `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){rel=""nofollow""} response support. This inherently brings compatibility with libraries like [Vercel/AI](https://github.com/vercel/ai){rel=""nofollow""}, which rely on streaming responses ([demo](https://github.com/Hebilicious/nuxt-openai-vercel-edge-demo){rel=""nofollow""}).
Leveraging this functionality is straightforward—simply return a [Readable Stream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream){rel=""nofollow""} or [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response){rel=""nofollow""} 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/){rel=""nofollow""} and [Nuxt](https://nuxt.com/){rel=""nofollow""}.
```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/){rel=""nofollow""} 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){rel=""nofollow""}.
- `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){rel=""nofollow""} 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){rel=""nofollow""} 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){rel=""nofollow""}
{without-border=""}
## Full Changelog
For a comprehensive list of changes, refer to the [release notes](https://github.com/unjs/h3/releases/tag/v1.8.0){rel=""nofollow""}.
# H3 v2 beta
::read-more{to="https://h3.dev/guide"}
Visit the new [**H3 Guide**](https://h3.dev/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){rel=""nofollow""}, we could run [Nitro](https://nitro.build){rel=""nofollow""} deployments in worker environments with Node.js compatibility, best of both worlds! Since [v1.8](https://h3.dev/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/){rel=""nofollow""} and runtime support in [Deno](https://deno.com/){rel=""nofollow""}, [Bun](https://bun.sh/){rel=""nofollow""}, and the latest [Node.js](https://nodejs.org/en){rel=""nofollow""}, 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){rel=""nofollow""} 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){rel=""nofollow""}).
Runtimes such as [Deno](https://deno.com/){rel=""nofollow""}, [Bun](https://bun.sh/){rel=""nofollow""}, 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){rel=""nofollow""}: 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){rel=""nofollow""} 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](https://h3.dev/#lighter-than-a-feather) than a feather.
- 👌 Intuitive [typed handlers](https://h3.dev/guide/basics/handler), [responses](https://h3.dev/guide/basics/response) and [errors](https://h3.dev/guide/basics/error).
- 🧩 Reusable [middleware](https://h3.dev/guide/basics/middleware) and [plugins](https://h3.dev/guide/advanced/plugins).
- 🌳 Fast [routing](https://h3.dev/guide/basics/routing).
- ➕ Built-in [utilities](https://h3.dev/utils).
- ❤️ Maximum [compatibility](https://h3.dev/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 :br Bun: 27 µs :br Deno: 7 ms | Node: **7 µs** (**5x faster**) :br Bun: **3 µs** (**9x faster**) :br Deno: **1.2 µs** (**156x faster**) |
| Bundle Size | min: 101 kB :br min+gzip: 39.6 kB | min: **9,1 kB** (**91% smaller**) :br min+gzip: **3.6 kB** (**90% smaller**) :br min: **5.2 kB** / min+gzip: **2.1 kB** ([fetchable](https://h3.dev/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){rel=""nofollow""} for details and [srvx benchmarks](https://github.com/h3js/srvx/tree/main/test/bench-node){rel=""nofollow""} 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){rel=""nofollow""}, [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response){rel=""nofollow""}, [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL){rel=""nofollow""}, and [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers){rel=""nofollow""}, 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){rel=""nofollow""}. 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`](https://h3.dev/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){rel=""nofollow""} 💛).
Additionally, we have introduced a simple yet powerful pattern to extend H3 apps using reusable [plugins](https://h3.dev/guide/advanced/plugins).
::CodeGroup
```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="https://h3.dev/migration"}
Check out [Migration Guide](https://h3.dev/migration).
::
## 🙌 Unified H(TTP) Server Tools for Everyone
H3 and related projects moved to a dedicated [github org](https://github.com/h3js){rel=""nofollow""} and new [h3.dev](https://h3.dev){rel=""nofollow""} domain (thanks to the donation from [syntax.fm](https://syntax.fm/){rel=""nofollow""} and other [sponsors](https://h3.dev/#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){rel=""nofollow""}: Minimal HTTP Framework.
- [🌳 rou3](https://github.com/h3js/rou3){rel=""nofollow""}: Lightweight JavaScript Router.
- [💥 srvx](https://srvx.h3.dev){rel=""nofollow""}: Universal Web-based Server API.
- [🔌 crossws](https://crossws.h3.dev){rel=""nofollow""}: Cross-platform WebSocket support.
## ❤️ Special Thanks
This release would not have been possible without wonderful [contributors](https://github.com/h3js/h3/graphs/contributors){rel=""nofollow""}, feedback from the [community](https://discord.h3.dev){rel=""nofollow""}, inspirations from web-standard frameworks including [Hono](https://hono.dev/){rel=""nofollow""} and [Elysia](https://elysiajs.com/){rel=""nofollow""}, and [sponsors](https://h3.dev/#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){rel=""nofollow""} v3.
::callout{to="https://discord.h3.dev"}
Join our [Discord](https://discord.h3.dev){rel=""nofollow""} to share your experience and feedback!
::
::read-more{to="https://h3.dev/guide"}
Visit the new [**H3 Guide**](https://h3.dev/guide) to get started quickly.
::
# Blog
H3 release highlights.