# 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 "\n

Executing...

    \n"; // Do work ... for (let i = 0; i < 1000; i++) { await delay(1000); // Report progress yield `
  1. Completed job #`; yield i; yield `
  2. \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("