Repository: unjs/h3
Pull request #829 (2024-07-17 15:32:37)

feat: web native reimplementation
History:
- h3 v0 was based on Node.js `(req, res)` API with unenv [direct fetch](https://github.com/unjs/unenv/blob/main/src/runtime/fetch/call.ts) allowing Nitro to support h3 in web `Request => Response` adapters
- h3 v1.8 introduced built-in `toWebHandler` powered by still unenv and supporting `Response`
- h3 v2 (previously) introduced multi-adapter Event classes with a unified wrapper and native `app.fetch` (#783, #792, #820, #822)

This PR completely upgrades h3 to be based around modern web `Request => Response` + `URL` + `Header` APIs (without intermediate abstraction) alongside multiple performance improvements by avoiding extra promise creation (up to 2x faster than initial v2!).

Node.js adapter now implements proxies for `Request`, `Response`, `URL`, and `Headers` giving full two-way sync between Node.js and Web APIs.

In order to simplify things, this PR also adds back `event.node?` to access node context in Node.js compatible environments.

ufo dependency is removed as now we use web search params for parsing query as well.

This upgrade improves performance and also makes a true web-native HTTP framework with first-class Node.js support! 


Patch:
diff --git a/MIGRATION.md b/MIGRATION.md
index 353fd60..40c6fbc 100644
--- a/MIGRATION.md
+++ b/MIGRATION.md
@@ -12,23 +12,21 @@ h3 v2 includes some behavior and API changes that you need to consider applying
 > [!NOTE]
 > This is an undergoing migration guide and is not finished yet.
 
-## Web adapter
+## Web stanrdards
 
-H3 v2 is now web-native and you can directly use `app.fetch(request, init)`.
+H3 v2 is rewritten based on Web standard primitives ([`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL), [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers), [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request), and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)).
 
-Old utils for plain handler and web handler are removed to embrace web standards.
-
-## Event interface
+`event.node` context is only available when running in Node.js runtime and `event.web` is available via `event.request`.
 
-Event Properties `event.node.{req,res}` and `event.web` are not available anymore, instead, you can use `getNodeContext(event)` and `getWebContext(event)` utils to access raw objects for each runtime.
+On Node.js runtime, h3 uses a two way proxy to sync Node.js API with Web standard API making it a seamless experience on Node.
 
-`event.handler` property is removed since h3 relies on explicit responses.
+Old utils for plain handler and web handler are removed to embrace web standards.
 
 ## Response handling
 
-You should always explicitly use `return` for response and `throw` for errors from event handlers.
+You should always explicitly `return` the response body.
 
-If you were previously using these methods, you can replace them with `return` statements returning a text, JSON, stream, or web `Response` (h3 smartly detects and handles them):
+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 <value>`.
 - `sendError(event, <error>)`: Migrate to `throw createError(<error>)`.
@@ -38,7 +36,7 @@ If you were previously using these methods, you can replace them with `return` s
 Other send utils that are renamed and need explicit `return`:
 
 - `sendNoContent(event)` / `return null`: Migrate to `return noContent(event)`.
-- `sendIterable(event, <value>)`: Migrate to `return iterable(<value>)`.
+- `sendIterable(event, <value>)`: Migrate to `return iterable(event, <value>)`.
 - `sendRedirect(event, location, code)`: Migrate to `return redirect(event, location, code)`.
 - `sendProxy(event, target)`: Migrate to `return proxy(event, target)`.
 - `handleCors(event)`: Check return value (boolean) and early `return` if handled.
@@ -46,54 +44,50 @@ Other send utils that are renamed and need explicit `return`:
 
 ## App interface and router
 
-Router functionality is now integrated into the h3 app core. Instead of `createApp()` and `createRouter()` you can use `const app = createH3()`.
+Router functionality is now integrated into the h3 app core. Instead of `createApp()` and `createRouter()` you can use `createH3()`.
 
-Methods:
+New methods:
 
-- `app.use(handler)`: Add a global middleware.
-- `app.use(route, handler)`: Add a routed middleware.
-- `app.on(method, handler)` / `app.all(handler)` / `app.[METHOD](handler)`: Add a route handler.
+- `app.use(handler)`: Adds a global middleware.
+- `app.use(route, handler)`: Adds a routed middleware.
+- `app.on(method, handler)` / `app.all(handler)` / `app.[METHOD](handler)`: Adds a route handler.
 
-Running order:
+Handlers will run in this order:
 
-- All global middleware in the same order they added
+- All global middleware in the same order were registered
 - All routed middleware from least specific to most specific paths (auto-sorted)
 - Matched route handler
 
-Any middleware or route handler can return a response.
+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.
+
+h3 migrated to a brand new route-matching engine [unjs/rou3](https://rou3.unjs.io/). You might experience slight (but more intuitive) behavior changes for matching patterns.
 
 Other changes from v1:
 
 - Handlers registered with `app.use("/path", handler)` only match `/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)`).
-- `app.use(() => handler, { lazy: true })` is no supported anymore. Instead you can use `app.use(defineLazyEventHandler(() => handler), { lazy: true })`
+- `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.
 - Custom `match` function for `app.use` is not supported anymore (middleware can skip themselves).
-- `app.resolve(path) => { route, handler }` changed to `app.resolve(method, path) => { method route, handler }`
-
-### Router
-
-h3 migrated to a brand new route-matching engine [unjs/rou3](https://rou3.unjs.io/).
-
-You might experience slight (and more intuitive) behavior changes for matching patterns.
-
+- `app.resolve(path) => { route, handler }` changed to `app.resolve(method, path) => { method, route, handler }`.
 - `router.use(path, handler)` is deprecated. Use `router.all(path, handler)` instead.
 - `router.add(path, method: Method | Method[]` signature is changed to `router.add(method: Method, path)` (**important**)
 
 ## Body utils
 
-The legacy `readBody` and `readRawBody` utils are replaced with a new set of body utils that can leverage native runtime primitives better.
+Most of request body utilities can now be replaced with `event.request` utils which is based on standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Response) interface.
+
+`readBody(event)` utility will use [`JSON.parse`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) or [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) for parsing requests with `application/x-www-form-urlencoded` content-type.
 
-- `readRawBody`: Returns body as [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) which is similar to Node.js `Buffer`.
-- `readTextBody`: Returns body as text (`utf8`).
-- `readJSONBody` / `readValidatedJSONBody`: Returns JSON value of the body using `JSON.parse` or `URLSearchParams` for `form-urlencoded`
-- `readFormDataBody`: Returns body parsed as [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData).
-- `getBodyStream`: Returns body as a [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream), replacing older experimental `getRequestWebStream`: Util.
+- For text: Use [event.request.text()](https://developer.mozilla.org/en-US/docs/Web/API/Request/text).
+- For json: Use [event.request.json()](https://developer.mozilla.org/en-US/docs/Web/API/Request/json).
+- For formData: Use [event.request.formData()](https://developer.mozilla.org/en-US/docs/Web/API/Request/formData).
+- For stream: Use [event.request.body](https://developer.mozilla.org/en-US/docs/Web/API/Request/body).
 
 **Behavior changes:**
 
-- Body utils won't throw an error if the incoming request has no body (or is a `GET` method for example) but instead, returns `undefined`
-- `readJSONBody` does not use [unjs/destr](https://destr.unjs.io) anymore. You should always filter and sanitize data coming from user to avoid [prototype-poisoning](https://medium.com/intrinsic-blog/javascript-prototype-poisoning-vulnerabilities-in-the-wild-7bc15347c96)
+- Body utils won't throw an error if the incoming request has no body (or is a `GET` method for example) but instead, return empty values.
+- native `request.json` and `readBody` does not use [unjs/destr](https://destr.unjs.io) anymore. You should always filter and sanitize data coming from user to avoid [prototype-poisoning](https://medium.com/intrinsic-blog/javascript-prototype-poisoning-vulnerabilities-in-the-wild-7bc15347c96).
 
 ## Cookie and headers
 
@@ -109,28 +103,31 @@ h3 v2 deprecated some legacy and aliased utilities.
 
 **App and router:**
 
-- `createRouter`: Migrate to `createH3`
-- `createApp`: Migrate to `createH3`
+- `createApp` / `createRouter`: Migrate to `createH3()`.
 
 **Handler:**
 
-- `eventHandler`: Migrate to `defineEventHandler`
-- `lazyEventHandler`: Migrate to `defineLazyEventHandler`
-- `useBase`: Migrate to `withbase`
+- `eventHandler`: Migrate to `defineEventHandler` (or remove it!).
+- `lazyEventHandler`: Migrate to `defineLazyEventHandler`.
 - `toEventHandler` / `isEventHandler`: (removed) Any function can be an event handler.
+- `useBase`: Migrate to `withbase`.
 
 **Request:**
 
-- `getHeader`: Migrate to `getRequestHeader`.
-- `getHeaders`: Migrate to `getRequestHeaders`.
-- `getRequestPath`: Migrate to `event.path`.
+- `getHeader` / `getRequestHeader`: Migrate to `event.request.headers.get(name)`.
+- `getHeaders` / `getRequestHeaders`: Migrate to `Object.fromEntries(event.request.headers.entries())`.
+- `getRequestPath`: Migrate to `event.path` or `event.url`.
+- `getMethod`: Migrate to `event.method`.
 
 **Response:**
 
-- `appendHeader`: Migrate to `appendResponseHeader`.
+- `getResponseHeader` / `getResponseHeaders`: Migrate to `event.response.headers.get(name)`
+- `setHeader` / `setResponseHeader` / `setHeaders` / `setResponseHeaders`: Migrate to `event.response.headers.set(name, value)`.
+- `appendHeader` / `appendResponseHeader` / `appendResponseHeaders`: Migrate to `event.response.headers.append(name, value)`.
+- `removeResponseHeader` / `clearResponseHeaders`: Migrate to `event.response.headers.delete(name)`
 - `appendHeaders`: Migrate to `appendResponseHeaders`.
-- `setHeader`: Migrate to `setResponseHeader`.
-- `setHeaders`: Migrate to `setResponseHeaders`.
+- `defaultContentType`: Migrate to `event.response.headers.set("content-type", type)`
+- `getResponseStatus` / `getResponseStatusText` / `setResponseStatus`: Use `event.response.status` and `event.response.statusText`.
 
 **Node.js:**
 
@@ -153,16 +150,14 @@ h3 v2 deprecated some legacy and aliased utilities.
 
 **Body:**
 
-- `readBody`: Migrate to `readJSONBody`.
-- `readFormData`: Migrate to `readFormDataBody`.
-- `readValidatedBody`: Migrate to `readValidatedJSONBody`.
-- `getRequestWebStream`: Migrate to `getBodyStream`.
-- `readMultipartFormData`: Migrate to `readFormDataBody`.
+- `readRawBody`: Migrate to `event.request.text()` or `event.request.arrayBuffer()`.
+- `getBodyStream` / `getRequestWebStream`: Migrate to `event.request.body`.
+- `readFormData` / `readMultipartFormData` / `readFormDataBody`: Migrate to `event.request.formData()`.
 
-- **Utils:**
+**Utils:**
 
-- `isStream`: Migrate to `instanceof ReadableStream` and `.pipe` properties for detecting Node.js `ReadableStream`.
-- `isWebResponse`: Migrate to `use instanceof Response`.
+- `isStream`: Migrate to `instanceof ReadableStream`.
+- `isWebResponse`: Migrate to `instanceof Response`.
 - `MIMES`: (removed).
 
 **Types:**
@@ -180,4 +175,4 @@ h3 v2 deprecated some legacy and aliased utilities.
 - `RouteNode`: Migrate to `RouterEntry`.
   `CreateRouterOptions`: Migrate to `RouterOptions`.
 
-Removed type exports: `WebEventContext`, `NodeEventContext`, `NodePromisifiedHandler`, `AppUse`, `Stack`, `InputLayer`, `InputStack`, `Layer`, `Matcher`, `PlainHandler`, `PlainRequest`, `PlainReponse`, `WebHandler`
+Removed type exports: `WebEventContext`, `NodeEventContext`, `NodePromisifiedHandler`, `AppUse`, `Stack`, `InputLayer`, `InputStack`, `Layer`, `Matcher`, `PlainHandler`, `PlainRequest`, `PlainResponse`, `WebHandler`.
diff --git a/build.config.ts b/build.config.ts
index 6ad3f67..d06c88c 100644
--- a/build.config.ts
+++ b/build.config.ts
@@ -5,6 +5,11 @@ export default defineBuildConfig({
   rollup: {
     esbuild: {
       target: "ES2022",
+      tsconfigRaw: {
+        compilerOptions: {
+          useDefineForClassFields: false,
+        },
+      },
     },
   },
 });
diff --git a/docs/2.utils/1.request.md b/docs/2.utils/1.request.md
index 174f744..0abc56c 100644
--- a/docs/2.utils/1.request.md
+++ b/docs/2.utils/1.request.md
@@ -27,7 +27,7 @@ app.use("/", (event) => {
 
 ### `getQuery(event)`
 
-Get query the params object from the request URL parsed with [unjs/ufo](https://ufo.unjs.io).
+Get query the params object from the request URL.
 
 **Example:**
 
@@ -37,32 +37,6 @@ app.use("/", (event) => {
 });
 ```
 
-### `getRequestHeader(event, name)`
-
-Get a request header by name.
-
-**Example:**
-
-```ts
-app.use("/", (event) => {
-  const contentType = getRequestHeader(event, "content-type"); // "application/json"
-});
-```
-
-### `getRequestHeaders(event)`
-
-Get the request headers object.
-
-Array headers are joined with a comma.
-
-**Example:**
-
-```ts
-app.use("/", (event) => {
-  const headers = getRequestHeaders(event); // { "content-type": "application/json", "x-custom-header": "value" }
-});
-```
-
 ### `getRequestHost(event, opts: { xForwardedHost? })`
 
 Get the request hostname.
@@ -145,7 +119,7 @@ app.use("/", (event) => {
 
 Get matched route params.
 
-If `decode` option is `true`, it will decode the matched route params using `decodeURI`.
+If `decode` option is `true`, it will decode the matched route params using `decodeURIComponent`.
 
 **Example:**
 
@@ -157,7 +131,7 @@ app.use("/", (event) => {
 
 ### `getValidatedQuery(event, validate)`
 
-Get the query param from the request URL parsed with [unjs/ufo](https://ufo.unjs.io) and validated with validate function.
+Get the query param from the request URL validated with validate function.
 
 You can use a simple function to validate the query object or a library like `zod` to define a schema.
 
@@ -249,25 +223,7 @@ Get a unique fingerprint for the incoming request.
 
 <!-- automd:jsdocs src="../../src/utils/body.ts" -->
 
-### `getBodyStream(event)`
-
-Captures a stream from a request.
-
-### `readFormDataBody(event)`
-
-Constructs a FormData object from an event, after converting it to a a web request.
-
-**Example:**
-
-```ts
-app.use("/", async (event) => {
-  const formData = await readFormDataBody(event);
-  const email = formData.get("email");
-  const password = formData.get("password");
-});
-```
-
-### `readJSONBody(event)`
+### `readBody(event)`
 
 Reads request body and tries to parse using JSON.parse or URLSearchParams.
 
@@ -275,37 +231,13 @@ Reads request body and tries to parse using JSON.parse or URLSearchParams.
 
 ```ts
 app.use("/", async (event) => {
-  const body = await readAndParseBody(event);
-});
-```
-
-### `readRawBody(event)`
-
-Reads body of the request and returns an Uint8Array of the raw body.
-
-**Example:**
-
-```ts
-app.use("/", async (event) => {
-  const body = await readRawBody(event);
-});
-```
-
-### `readTextBody(event)`
-
-Reads body of the request and returns an string (utf-8) of the raw body.
-
-**Example:**
-
-```ts
-app.use("/", async (event) => {
-  const body = await readTextBody(event);
+  const body = await readBody(event);
 });
 ```
 
-### `readValidatedJSONBody(event, validate)`
+### `readValidatedBody(event, validate)`
 
-Tries to read the request body via `readJSONBody`, then uses the provided validation function and either throws a validation error or returns the result.
+Tries to read the request body via `readBody`, then uses the provided validation function and either throws a validation error or returns the result.
 
 You can use a simple function to validate the body or use a library like `zod` to define a schema.
 
@@ -313,7 +245,7 @@ You can use a simple function to validate the body or use a library like `zod` t
 
 ```ts
 app.use("/", async (event) => {
-  const body = await readValidatedJSONBody(event, (body) => {
+  const body = await readValidatedBody(event, (body) => {
     return typeof body === "object" && body !== null;
   });
 });
@@ -325,7 +257,7 @@ app.use("/", async (event) => {
 import { z } from "zod";
 app.use("/", async (event) => {
   const objectSchema = z.object();
-  const body = await readValidatedJSONBody(event, objectSchema.safeParse);
+  const body = await readValidatedBody(event, objectSchema.safeParse);
 });
 ```
 
diff --git a/docs/2.utils/2.response.md b/docs/2.utils/2.response.md
index 4f67bc2..8fea77e 100644
--- a/docs/2.utils/2.response.md
+++ b/docs/2.utils/2.response.md
@@ -8,90 +8,7 @@ icon: material-symbols-light:output
 
 <!-- automd:jsdocs src="../../src/utils/response.ts" -->
 
-### `appendResponseHeader(event, name, value)`
-
-Append a response header by name.
-
-**Example:**
-
-```ts
-app.use("/", (event) => {
-  appendResponseHeader(event, "content-type", "text/html");
-});
-```
-
-### `appendResponseHeaders(event, headers)`
-
-Append the response headers.
-
-**Example:**
-
-```ts
-app.use("/", (event) => {
-  appendResponseHeaders(event, {
-    "content-type": "text/html",
-    "cache-control": "no-cache",
-  });
-});
-```
-
-### `clearResponseHeaders(event, headerNames?)`
-
-Remove all response headers, or only those specified in the headerNames array.
-
-**Example:**
-
-```ts
-app.use("/", (event) => {
-  clearResponseHeaders(event, ["content-type", "cache-control"]); // Remove content-type and cache-control headers
-});
-```
-
-### `defaultContentType(event, type?)`
-
-Set the response status code and message.
-
-### `getResponseHeader(event, name)`
-
-### `getResponseHeaders(event)`
-
-Get the response headers object.
-
-**Example:**
-
-```ts
-app.use("/", (event) => {
-  const headers = getResponseHeaders(event);
-});
-```
-
-### `getResponseStatus(event)`
-
-Get the current response status code.
-
-**Example:**
-
-```ts
-app.use("/", (event) => {
-  const status = getResponseStatus(event);
-  return `Status: ${status}`;
-});
-```
-
-### `getResponseStatusText(event)`
-
-Get the current response status message.
-
-**Example:**
-
-```ts
-app.use("/", (event) => {
-  const statusText = getResponseStatusText(event);
-  return `Status: ${statusText}`;
-});
-```
-
-### `iterable(iterable)`
+### `iterable(_event, iterable)`
 
 Iterate a source of chunks and send back each chunk in order. Supports mixing async work together with emitting chunks.
 
@@ -102,7 +19,7 @@ For generator (yielding) functions, the returned value is treated the same as yi
 **Example:**
 
 ```ts
-return iterable(async function* work() {
+return iterable(event, async function* work() {
   // Open document body
   yield "<!DOCTYPE html>\n<html><body><h1>Executing...</h1><ol>\n";
   // Do work ...
@@ -155,58 +72,6 @@ app.use("/", (event) => {
 });
 ```
 
-### `removeResponseHeader(event, name)`
-
-Remove a response header by name.
-
-**Example:**
-
-```ts
-app.use("/", (event) => {
-  removeResponseHeader(event, "content-type"); // Remove content-type header
-});
-```
-
-### `setResponseHeader(event, name, value)`
-
-Set a response header by name.
-
-**Example:**
-
-```ts
-app.use("/", (event) => {
-  setResponseHeader(event, "content-type", "text/html");
-});
-```
-
-### `setResponseHeaders(event, headers)`
-
-Set the response headers.
-
-**Example:**
-
-```ts
-app.use("/", (event) => {
-  setResponseHeaders(event, {
-    "content-type": "text/html",
-    "cache-control": "no-cache",
-  });
-});
-```
-
-### `setResponseStatus(event, code?, text?)`
-
-Set the response status code and message.
-
-**Example:**
-
-```ts
-app.use("/", (event) => {
-  setResponseStatus(event, 404, "Not Found");
-  return "Not Found";
-});
-```
-
 ### `writeEarlyHints(event, hints)`
 
 Write `HTTP/1.1 103 Early Hints` to the client.
diff --git a/package.json b/package.json
index 878bd47..e140afe 100644
--- a/package.json
+++ b/package.json
@@ -44,7 +44,6 @@
     "iron-webcrypto": "^1.2.1",
     "ohash": "^1.1.3",
     "rou3": "^0.4.0",
-    "ufo": "^1.5.4",
     "uncrypto": "^0.1.3"
   },
   "devDependencies": {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 049e87a..7fcff37 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -23,9 +23,6 @@ importers:
       rou3:
         specifier: ^0.4.0
         version: 0.4.0
-      ufo:
-        specifier: ^1.5.4
-        version: 1.5.4
       uncrypto:
         specifier: ^0.1.3
         version: 0.1.3
diff --git a/src/_deprecated.ts b/src/_deprecated.ts
index 5fbfe5a..332cf5e 100644
--- a/src/_deprecated.ts
+++ b/src/_deprecated.ts
@@ -3,113 +3,261 @@ import type {
   H3Event,
   NodeHandler,
   RequestHeaderName,
-  RequestHeaders,
   RequestMiddleware,
   ResponseHeaderName,
   ResponseHeaders,
   ResponseMiddleware,
+  RequestHeaders,
 } from "./types";
-import type { Readable as NodeReadableStream } from "node:stream";
-import { _kRaw } from "./event";
-import { getRequestHeader, getRequestHeaders } from "./utils/request";
-import { getBodyStream } from "./utils/body";
-import {
-  appendResponseHeader,
-  appendResponseHeaders,
-  iterable,
-  noContent,
-  redirect,
-  setResponseHeader,
-  setResponseHeaders,
-} from "./utils/response";
+import { iterable, noContent, redirect } from "./utils/response";
 import {
   defineNodeHandler,
   fromNodeHandler,
   toNodeHandler,
 } from "./adapters/node";
-import {
-  readFormDataBody,
-  readJSONBody,
-  readValidatedJSONBody,
-} from "./utils/body";
 import { defineEventHandler, defineLazyEventHandler } from "./handler";
 import { proxy } from "./utils/proxy";
 import { createH3 } from "./h3";
 import { withBase } from "./utils/base";
+import { sanitizeStatusCode, sanitizeStatusMessage } from "./utils/sanitize";
+
+// --- Request ---
 
-/** @deprecated Please use `getRequestHeader`  */
+/** @deprecated Please use `event.path` or `event.url` */
+export const getRequestPath = (event: H3Event) => event.path;
+
+/** @deprecated Please use `event.request.headers.get(name)` */
+export function getRequestHeader(
+  event: H3Event,
+  name: string,
+): string | undefined {
+  return event.request.headers.get(name) || undefined;
+}
+
+/** @deprecated Please use `event.request.headers.get(name)` */
 export const getHeader = getRequestHeader;
 
-/** @deprecated Please use `getRequestHeaders`  */
+/** @deprecated Please use `Object.fromEntries(event.request.headers.entries())` */
+export function getRequestHeaders(event: H3Event): Record<string, string> {
+  return Object.fromEntries(event.request.headers.entries());
+}
+
+/** @deprecated Please use `Object.fromEntries(event.request.headers.entries())` */
 export const getHeaders = getRequestHeaders;
 
-/** @deprecated Directly return stream */
+/** @deprecated Please use `event.request.method` */
+export function getMethod(event: H3Event, defaultMethod = "GET") {
+  return (event.request.method || defaultMethod).toUpperCase();
+}
+
+// --- Request Body ---
+
+/** @deprecated Please use `event.request.text()` or `event.request.arrayBuffer()` */
+export function readRawBody<E extends "utf8" | false = "utf8">(
+  event: H3Event,
+  encoding = "utf8" as E,
+): E extends false
+  ? Promise<Uint8Array | undefined>
+  : Promise<string | undefined> {
+  return encoding
+    ? event.request.text()
+    : (event.request.arrayBuffer().then((r) => new Uint8Array(r)) as any);
+}
+
+/** @deprecated Please use `event.request.formData()` */
+export async function readFormDataBody(event: H3Event): Promise<FormData> {
+  return event.request.formData();
+}
+
+/** @deprecated Please use `event.request.formData()` */
+export const readFormData = readFormDataBody;
+
+/** @deprecated Please use `event.request.body` */
+export function getBodyStream(
+  event: H3Event,
+): ReadableStream<Uint8Array> | undefined {
+  return event.request.body || undefined;
+}
+
+/** @deprecated Please use `event.request.body` */
+export const getRequestWebStream = getBodyStream;
+
+// --- Response ---
+
+/** @deprecated Please directly return stream */
 export function sendStream(
   _event: H3Event,
-  value: ReadableStream | NodeReadableStream,
-) {
+  value: ReadableStream,
+): ReadableStream {
   return value;
 }
 
-/** Please use `defineEventHandler`  */
-export const eventHandler = defineEventHandler;
+/** @deprecated Please use `return noContent(event)` */
+export const sendNoContent = noContent;
 
-/** Please use `defineLazyEventHandler` */
-export const lazyEventHandler = defineLazyEventHandler;
+/** @deprecated Please use `return redirect(event, code)` */
+export const sendRedirect = redirect;
 
-/** @deprecated Please use `appendResponseHeader` */
+/** @deprecated Please directly return response */
+export const sendWebResponse = (response: Response) => response;
+
+/** @deprecated Please use `return proxy(event)` */
+export const sendProxy = proxy;
+
+/** @deprecated Please use `return iterable(event, value)` */
+export const sendIterable = iterable;
+
+/** @deprecated Please use `event.response.statusText` */
+export function getResponseStatusText(event: H3Event): string {
+  return event.response.statusText || "";
+}
+
+/** @deprecated Please use `event.response.headers.append(name, value)` */
+export function appendResponseHeader(
+  event: H3Event,
+  name: string,
+  value: string | string[],
+): void {
+  if (Array.isArray(value)) {
+    for (const valueItem of value) {
+      event.response.headers.append(name, valueItem!);
+    }
+  } else {
+    event.response.headers.append(name, value!);
+  }
+}
+
+/** @deprecated Please use `event.response.headers.append(name, value)` */
 export const appendHeader = appendResponseHeader;
 
-/** @deprecated Please use  `appendResponseHeaders` */
-export const appendHeaders = appendResponseHeaders;
+/** @deprecated Please use `event.response.headers.set(name, value)` */
+export function setResponseHeader(
+  event: H3Event,
+  name: string,
+  value: string | string[],
+): void {
+  if (Array.isArray(value)) {
+    event.response.headers.delete(name);
+    for (const valueItem of value) {
+      event.response.headers.append(name, valueItem!);
+    }
+  } else {
+    event.response.headers.set(name, value!);
+  }
+}
 
-/** @deprecated please use `setResponseHeader`  */
+/** @deprecated Please use `event.response.headers.set(name, value)` */
 export const setHeader = setResponseHeader;
 
-/** @deprecated Please use `setResponseHeaders` */
+/** @deprecated Please use `event.response.headers.set(name, value)` */
+export function setResponseHeaders(
+  event: H3Event,
+  headers: ResponseHeaders,
+): void {
+  for (const [name, value] of Object.entries(headers)) {
+    event.response.headers.set(name, value!);
+  }
+}
+
+/** @deprecated Please use `event.response.headers.set(name, value)` */
 export const setHeaders = setResponseHeaders;
 
-/** @deprecated Please use `defineNodeHandler` */
-export const defineNodeListener = defineNodeHandler;
+/** @deprecated Please use `event.response.status` */
+export function getResponseStatus(event: H3Event): number {
+  return event.response.status || 200;
+}
 
-/** @deprecated Please use `defineNodeHandler` */
-export const fromNodeMiddleware = fromNodeHandler;
+/** @deprecated Please directly set `event.response.status` and `event.response.statusText` */
+export function setResponseStatus(
+  event: H3Event,
+  code?: number,
+  text?: string,
+): void {
+  if (code) {
+    event.response.status = sanitizeStatusCode(code, event.response.status);
+  }
+  if (text) {
+    event.response.statusText = sanitizeStatusMessage(text);
+  }
+}
 
-/** @deprecated Please use `toNodeHandler` */
-export const toNodeListener = toNodeHandler;
+/** @deprecated Please use `event.response.headers.set("content-type", type)` */
+export function defaultContentType(event: H3Event, type?: string) {
+  if (
+    type &&
+    event.response.status !== 304 /* unjs/h3#603 */ &&
+    !event.response.headers.has("content-type")
+  ) {
+    event.response.headers.set("content-type", type);
+  }
+}
 
-/** @deprecated Please use `readJSONBody` */
-export const readBody = readJSONBody;
+/** @deprecated Please use `Object.fromEntries(event.response.headers.entries())` */
+export function getResponseHeaders(event: H3Event): Record<string, string> {
+  return Object.fromEntries(event.response.headers.entries());
+}
 
-/** @deprecated Please use `readFormDataBody` */
-export const readFormData = readFormDataBody;
+/** @deprecated Please use `event.response.headers.get(name)` */
+export function getResponseHeader(
+  event: H3Event,
+  name: string,
+): string | undefined {
+  return event.response.headers.get(name) || undefined;
+}
 
-/** @deprecated Please use `readValidatedJSONBody` */
-export const readValidatedBody = readValidatedJSONBody;
+/** @deprecated Please use `event.response.headers.delete(name)` instead. */
+export function removeResponseHeader(
+  event: H3Event,
+  name: ResponseHeaderName,
+): void {
+  return event.response.headers.delete(name);
+}
 
-/** @deprecated Please use `getBodyStream` */
-export const getRequestWebStream = getBodyStream;
+/** @deprecated Please use `event.response.headers.append(name, value)` */
+export function appendResponseHeaders(
+  event: H3Event,
+  headers: ResponseHeaders,
+): void {
+  for (const [name, value] of Object.entries(headers)) {
+    appendResponseHeader(event, name, value!);
+  }
+}
 
-/** @deprecated Please use `event.path` instead */
-export const getRequestPath = (event: H3Event) => event.path;
+/** @deprecated Please use `event.response.headers.append(name, value)` */
+export const appendHeaders = appendResponseHeaders;
 
-/** @deprecated Use `return iterable()` */
-export const sendIterable = (
-  _event: H3Event,
-  ...args: Parameters<typeof iterable>
-) => iterable(...args);
+/** @deprecated Please use `event.response.headers.delete` */
+export function clearResponseHeaders(
+  event: H3Event,
+  headerNames?: ResponseHeaderName[],
+): void {
+  if (headerNames && headerNames.length > 0) {
+    for (const name of headerNames) {
+      event.response.headers.delete(name);
+    }
+  } else {
+    for (const name of event.response.headers.keys()) {
+      event.response.headers.delete(name);
+    }
+  }
+}
 
-/** @deprecated Use `return noContent(event)` */
-export const sendNoContent = noContent;
+// -- Event handler --
 
-/** @deprecated Use `return redirect(event, code)` */
-export const sendRedirect = redirect;
+/** Please use `defineEventHandler`  */
+export const eventHandler = defineEventHandler;
 
-/** @deprecated Use `return response` */
-export const sendWebResponse = (response: Response) => response;
+/** Please use `defineLazyEventHandler` */
+export const lazyEventHandler = defineLazyEventHandler;
 
-/** @deprecated Use `return proxy(event)` */
-export const sendProxy = proxy;
+/** @deprecated Please use `defineNodeHandler` */
+export const defineNodeListener = defineNodeHandler;
+
+/** @deprecated Please use `defineNodeHandler` */
+export const fromNodeMiddleware = fromNodeHandler;
+
+/** @deprecated Please use `toNodeHandler` */
+export const toNodeListener = toNodeHandler;
 
 /** @deprecated */
 export function toEventHandler(
@@ -120,13 +268,15 @@ export function toEventHandler(
   return input;
 }
 
-/** @deprecated Use `createH3()` */
+// -- App/Router --
+
+/** @deprecated Please use `createH3()` */
 export const createApp = createH3;
 
-/** @deprecated Use `createH3()` */
+/** @deprecated Please use `createH3()` */
 export const createRouter = createH3;
 
-/** @deprecated Use `withBase()` */
+/** @deprecated Please use `withBase()` */
 export const useBase = withBase;
 
 // --- Types ---
diff --git a/src/adapters/base/event.ts b/src/adapters/base/event.ts
new file mode 100644
index 0000000..b49385f
--- /dev/null
+++ b/src/adapters/base/event.ts
@@ -0,0 +1,34 @@
+import type { H3EventContext, H3Event, HTTPMethod } from "../../types";
+
+export abstract class BaseEvent implements Partial<H3Event> {
+  static __is_event__ = true;
+
+  abstract url: URL;
+  abstract request: Request;
+
+  context: H3EventContext;
+
+  constructor(context?: H3EventContext) {
+    this.context = context || Object.create(null);
+  }
+
+  get method(): HTTPMethod {
+    return this.request.method as HTTPMethod;
+  }
+
+  get path(): string {
+    return this.url.pathname + this.url.search;
+  }
+
+  get headers(): Headers {
+    return this.request.headers;
+  }
+
+  toString(): string {
+    return `[${this.request.method}] ${this.request.url}`;
+  }
+
+  toJSON(): string {
+    return this.toString();
+  }
+}
diff --git a/src/adapters/node/_event.ts b/src/adapters/node/_event.ts
deleted file mode 100644
index e3f6ea9..0000000
--- a/src/adapters/node/_event.ts
+++ /dev/null
@@ -1,191 +0,0 @@
-import type { HTTPMethod } from "../../types";
-import type { RawEvent } from "../../types/event";
-import { splitCookiesString } from "../../utils/cookie";
-import { NodeHeadersProxy } from "./_headers";
-import { readNodeReqBody, getBodyStream } from "./_utils";
-
-import type { NodeIncomingMessage, NodeServerResponse } from "../../types/node";
-
-export class NodeEvent implements RawEvent {
-  static isNode = true;
-
-  _req: NodeIncomingMessage;
-  _res: NodeServerResponse;
-
-  _originalPath?: string | undefined;
-
-  _rawBody?: Promise<undefined | Uint8Array>;
-  _textBody?: Promise<undefined | string>;
-  _formDataBody?: Promise<undefined | FormData>;
-  _bodyStream?: undefined | ReadableStream<Uint8Array>;
-
-  _headers?: NodeHeadersProxy;
-  _responseHeaders?: NodeHeadersProxy;
-
-  constructor(req: NodeIncomingMessage, res: NodeServerResponse) {
-    this._req = req;
-    this._res = res;
-  }
-
-  getContext() {
-    return {
-      req: this._req,
-      res: this._res,
-    };
-  }
-
-  // -- request --
-
-  get method() {
-    return this._req.method as HTTPMethod;
-  }
-
-  get path() {
-    return this._req.url || "/";
-  }
-
-  set path(path: string) {
-    if (!this.originalPath) {
-      this._originalPath = this.path;
-    }
-    this._req.url = path;
-  }
-
-  get originalPath() {
-    return this._originalPath || this.path;
-  }
-
-  getHeader(key: string) {
-    const value = this._req.headers[key];
-    if (Array.isArray(value)) {
-      return value.join(", ");
-    }
-    return value;
-  }
-
-  getHeaders() {
-    if (!this._headers) {
-      this._headers = new NodeHeadersProxy(() => this._req.headers);
-    }
-    return this._headers;
-  }
-
-  get remoteAddress() {
-    return this._req.socket.remoteAddress;
-  }
-
-  get isSecure() {
-    return (this._req.connection as any).encrypted;
-  }
-
-  readRawBody() {
-    if (!this._rawBody) {
-      this._rawBody = readNodeReqBody(this._req);
-    }
-    return this._rawBody;
-  }
-
-  readTextBody() {
-    if (this._textBody) {
-      return this._textBody;
-    }
-    this._textBody = Promise.resolve(this.readRawBody()).then((body) =>
-      body ? new TextDecoder().decode(body) : undefined,
-    );
-    return this._textBody;
-  }
-
-  async readFormDataBody() {
-    if (!this._formDataBody) {
-      this._formDataBody = Promise.resolve(this.readRawBody()).then((body) =>
-        body
-          ? new Response(body, {
-              headers: this.getHeaders(),
-            }).formData()
-          : undefined,
-      );
-    }
-    return this._formDataBody;
-  }
-
-  getBodyStream() {
-    if (!this._bodyStream) {
-      this._bodyStream = getBodyStream(this._req);
-    }
-    return this._bodyStream;
-  }
-
-  // -- response --
-
-  get handled() {
-    return this._res.writableEnded || this._res.headersSent;
-  }
-
-  get responseCode() {
-    return this._res.statusCode || 200;
-  }
-
-  set responseCode(code: number) {
-    this._res.statusCode = code;
-  }
-
-  get responseMessage() {
-    return this._res.statusMessage;
-  }
-
-  set responseMessage(message: string) {
-    this._res.statusMessage = message;
-  }
-
-  setResponseHeader(key: string, value: string) {
-    this._res.setHeader(key, value);
-  }
-
-  appendResponseHeader(key: string, value: string) {
-    this._res.appendHeader(key, value);
-  }
-
-  getResponseHeader(key: string) {
-    const value = this._res.getHeader(key);
-    if (!value) {
-      return undefined;
-    }
-    if (Array.isArray(value)) {
-      return value.join(", ");
-    }
-    return value as string;
-  }
-
-  getResponseHeaders() {
-    if (!this._responseHeaders) {
-      this._responseHeaders = new NodeHeadersProxy(() =>
-        this._res.getHeaders(),
-      );
-    }
-    return this._responseHeaders;
-  }
-
-  getResponseSetCookie() {
-    const value = this._res.getHeader("set-cookie");
-    if (!value) {
-      return [];
-    }
-    return splitCookiesString(value as string | string[]);
-  }
-
-  removeResponseHeader(key: string) {
-    this._res.removeHeader(key);
-  }
-
-  writeHead(code: number, message?: string) {
-    this._res.writeHead(code, message);
-  }
-
-  writeEarlyHints(hints: Record<string, string | string[]>) {
-    if (this._res.writeEarlyHints) {
-      return new Promise<void>((resolve) => {
-        return this._res.writeEarlyHints(hints, resolve);
-      });
-    }
-  }
-}
diff --git a/src/adapters/node/_headers.ts b/src/adapters/node/_headers.ts
deleted file mode 100644
index ffd2ead..0000000
--- a/src/adapters/node/_headers.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-import type { OutgoingHttpHeaders, IncomingHttpHeaders } from "node:http";
-import { splitCookiesString } from "../../utils/cookie";
-
-type NodeHeaders = OutgoingHttpHeaders | IncomingHttpHeaders;
-
-export class NodeHeadersProxy implements Headers {
-  getHeaders: () => NodeHeaders;
-
-  constructor(getHeaders: () => NodeHeaders) {
-    this.getHeaders = getHeaders;
-  }
-
-  append(name: string, value: string): void {
-    const _headers = this.getHeaders();
-    const _current = _headers[name];
-    if (_current) {
-      if (Array.isArray(_current)) {
-        _current.push(value);
-      } else {
-        _headers[name] = [_current as string, value];
-      }
-    } else {
-      _headers[name] = value;
-    }
-  }
-
-  delete(name: string): void {
-    this.getHeaders()[name] = undefined;
-  }
-
-  get(name: string): string | null {
-    return _normalizeValue(this.getHeaders()[name]);
-  }
-
-  getSetCookie(): string[] {
-    const setCookie = this.getHeaders()["set-cookie"];
-    if (!setCookie || setCookie.length === 0) {
-      return [];
-    }
-    return splitCookiesString(setCookie);
-  }
-
-  has(name: string): boolean {
-    return !!this.getHeaders()[name];
-  }
-
-  set(name: string, value: string): void {
-    this.getHeaders()[name] = value;
-  }
-
-  forEach(
-    cb: (value: string, key: string, parent: Headers) => void,
-    thisArg?: any,
-  ): void {
-    const _headers = this.getHeaders();
-    for (const key in _headers) {
-      if (_headers[key]) {
-        cb.call(thisArg, _normalizeValue(_headers[key]), key, this);
-      }
-    }
-  }
-
-  *entries(): IterableIterator<[string, string]> {
-    const _headers = this.getHeaders();
-    for (const key in _headers) {
-      yield [key, _normalizeValue(_headers[key])];
-    }
-  }
-
-  *keys(): IterableIterator<string> {
-    const keys = Object.keys(this.getHeaders());
-    for (const key of keys) {
-      yield key;
-    }
-  }
-
-  *values(): IterableIterator<string> {
-    const values = Object.values(this.getHeaders());
-    for (const value of values) {
-      yield _normalizeValue(value);
-    }
-  }
-
-  [Symbol.iterator](): IterableIterator<[string, string]> {
-    return this.entries()[Symbol.iterator]();
-  }
-}
-
-function _normalizeValue(
-  value: string | string[] | number | undefined,
-): string {
-  if (Array.isArray(value)) {
-    return value.join(", ");
-  }
-  return (value as string) || "";
-}
diff --git a/src/adapters/node/event.ts b/src/adapters/node/event.ts
new file mode 100644
index 0000000..360f041
--- /dev/null
+++ b/src/adapters/node/event.ts
@@ -0,0 +1,48 @@
+import type { H3Event, H3EventContext, HTTPMethod } from "../../types";
+import type { IncomingMessage, ServerResponse } from "node:http";
+import { BaseEvent } from "../base/event";
+import { kNodeInspect } from "./internal/utils";
+import { NodeRequestProxy } from "./internal/request";
+import { NodeResponseProxy } from "./internal/response";
+
+export const NodeEvent = /* @__PURE__ */ (() =>
+  class NodeEvent extends BaseEvent implements H3Event {
+    request: Request;
+    url: URL;
+    response: H3Event["response"];
+
+    node: { req: IncomingMessage; res: ServerResponse };
+
+    constructor(
+      req: IncomingMessage,
+      res: ServerResponse,
+      context?: H3EventContext,
+    ) {
+      super(context);
+      this.node = { req, res };
+      const request = new NodeRequestProxy(req);
+      this.request = request;
+      this.url = request._url;
+      this.response = new NodeResponseProxy(res);
+    }
+
+    get path(): string {
+      return this.node.req.url || "/";
+    }
+
+    get method(): HTTPMethod {
+      return this.node.req.method as HTTPMethod;
+    }
+
+    get ip(): string | undefined {
+      return this.node.req.socket?.remoteAddress;
+    }
+
+    [kNodeInspect]() {
+      return {
+        context: this.context,
+        request: this.request,
+        response: this.response,
+      };
+    }
+  })();
diff --git a/src/adapters/node/index.ts b/src/adapters/node/index.ts
index 166244f..6532552 100644
--- a/src/adapters/node/index.ts
+++ b/src/adapters/node/index.ts
@@ -1,49 +1,57 @@
-import type {
-  H3,
-  EventHandler,
-  EventHandlerResponse,
-  H3Event,
-} from "../../types";
+import type { H3, EventHandler, EventHandlerResponse } from "../../types";
 import type { NodeHandler, NodeMiddleware } from "../../types/node";
-import { _kRaw } from "../../event";
-import { EventWrapper } from "../../event";
-import { NodeEvent } from "./_event";
-import { sendNodeResponse, callNodeHandler } from "./_utils";
-import { errorToH3Response, prepareResponse } from "../../response";
+import { NodeEvent } from "./event";
+import { sendNodeResponse, callNodeHandler } from "./internal/utils";
+import { prepareResponse, errorToRes } from "../../response";
+
+export { kNodeReq, kNodeRes } from "./internal/utils";
 
 /**
  * Convert H3 app instance to a NodeHandler with (IncomingMessage, ServerResponse) => void signature.
  */
 export function toNodeHandler(app: H3): NodeHandler {
   const nodeHandler: NodeHandler = async function (nodeReq, nodeRes) {
-    const rawEvent = new NodeEvent(nodeReq, nodeRes);
-    const event = new EventWrapper(rawEvent);
-
-    const _res = await app.handler(event).catch((error: unknown) => error);
+    const event = new NodeEvent(nodeReq, nodeRes);
 
-    if (nodeRes.headersSent || nodeRes.writableEnded) {
-      return;
+    let _handlerRes: unknown | Promise<unknown>;
+    try {
+      _handlerRes = app._handler(event);
+    } catch (error: any) {
+      _handlerRes = error;
     }
 
-    const resBody = await prepareResponse(event, _res, app.config);
+    const _body =
+      _handlerRes instanceof Promise
+        ? _handlerRes
+            .catch((error) => error)
+            .then((_resolvedRes) =>
+              prepareResponse(event, _resolvedRes, app.config),
+            )
+        : prepareResponse(event, _handlerRes, app.config);
 
-    await sendNodeResponse(nodeRes, resBody).catch((sendError) => {
-      // Possible cases: Stream canceled, headers already sent, etc.
-      if (nodeRes.headersSent || nodeRes.writableEnded) {
-        return;
-      }
-      const errRes = errorToH3Response(sendError, app.config);
-      if (errRes.status) {
-        nodeRes.statusCode = errRes.status;
-      }
-      if (errRes.statusText) {
-        nodeRes.statusMessage = errRes.statusText;
-      }
-      nodeRes.end(errRes.body);
-    });
-    if (app.config.onAfterResponse) {
-      await app.config.onAfterResponse(event, { body: resBody });
-    }
+    return Promise.resolve(_body)
+      .then((body) => {
+        const _chain = sendNodeResponse(nodeRes, body);
+        return app.config.onAfterResponse
+          ? Promise.resolve(_chain).then(() =>
+              app.config.onAfterResponse!(event, { body }),
+            )
+          : _chain;
+      })
+      .catch((error) => {
+        // Possible cases: Stream canceled, headers already sent, etc.
+        if (nodeRes.headersSent || nodeRes.writableEnded) {
+          return;
+        }
+        const errRes = errorToRes(error, app.config);
+        if (errRes.status) {
+          nodeRes.statusCode = errRes.status;
+        }
+        if (errRes.statusText) {
+          nodeRes.statusMessage = errRes.statusText;
+        }
+        nodeRes.end(errRes.body);
+      });
   };
 
   return nodeHandler;
@@ -63,30 +71,19 @@ export function fromNodeHandler(
     throw new TypeError(`Invalid handler. It should be a function: ${handler}`);
   }
   return (event) => {
-    const nodeCtx = getNodeContext(event);
-    if (!nodeCtx) {
+    if (!event.node) {
       throw new Error(
         "[h3] Executing Node.js middleware is not supported in this server!",
       );
     }
     return callNodeHandler(
       handler,
-      nodeCtx.req,
-      nodeCtx.res,
+      event.node.req,
+      event.node.res,
     ) as EventHandlerResponse;
   };
 }
 
-export function getNodeContext(
-  event: H3Event,
-): undefined | ReturnType<NodeEvent["getContext"]> {
-  const raw = event[_kRaw] as NodeEvent;
-  if (!(raw?.constructor as any)?.isNode) {
-    return undefined;
-  }
-  return raw.getContext();
-}
-
 export function defineNodeHandler(handler: NodeHandler) {
   return handler;
 }
diff --git a/src/adapters/node/internal/headers.ts b/src/adapters/node/internal/headers.ts
new file mode 100644
index 0000000..f1e7035
--- /dev/null
+++ b/src/adapters/node/internal/headers.ts
@@ -0,0 +1,186 @@
+import type { IncomingMessage, ServerResponse } from "node:http";
+import { splitCookiesString } from "../../../utils/cookie";
+import { kNodeInspect, kNodeReq, kNodeRes } from "./utils";
+
+export const NodeReqHeadersProxy = /* @__PURE__ */ (() =>
+  class NodeReqHeadersProxy implements Headers {
+    [kNodeReq]: IncomingMessage;
+
+    constructor(req: IncomingMessage) {
+      this[kNodeReq] = req;
+    }
+
+    append(name: string, value: string): void {
+      const _headers = this[kNodeReq].headers;
+      const _current = _headers[name];
+      if (_current) {
+        if (Array.isArray(_current)) {
+          _current.push(value);
+        } else {
+          _headers[name] = [_current as string, value];
+        }
+      } else {
+        _headers[name] = value;
+      }
+    }
+
+    delete(name: string): void {
+      this[kNodeReq].headers[name] = undefined;
+    }
+
+    get(name: string): string | null {
+      return _normalizeValue(this[kNodeReq].headers[name]);
+    }
+
+    getSetCookie(): string[] {
+      const setCookie = this[kNodeReq].headers["set-cookie"];
+      if (!setCookie || setCookie.length === 0) {
+        return [];
+      }
+      return splitCookiesString(setCookie);
+    }
+
+    has(name: string): boolean {
+      return !!this[kNodeReq].headers[name];
+    }
+
+    set(name: string, value: string): void {
+      this[kNodeReq].headers[name] = value;
+    }
+
+    forEach(
+      cb: (value: string, key: string, parent: Headers) => void,
+      thisArg?: any,
+    ): void {
+      const _headers = this[kNodeReq].headers;
+      for (const key in _headers) {
+        if (_headers[key]) {
+          cb.call(thisArg, _normalizeValue(_headers[key]), key, this);
+        }
+      }
+    }
+
+    *entries(): IterableIterator<[string, string]> {
+      const _headers = this[kNodeReq].headers;
+      for (const key in _headers) {
+        yield [key, _normalizeValue(_headers[key])];
+      }
+    }
+
+    *keys(): IterableIterator<string> {
+      const keys = Object.keys(this[kNodeReq].headers);
+      for (const key of keys) {
+        yield key;
+      }
+    }
+
+    *values(): IterableIterator<string> {
+      const values = Object.values(this[kNodeReq].headers);
+      for (const value of values) {
+        yield _normalizeValue(value);
+      }
+    }
+
+    [Symbol.iterator](): IterableIterator<[string, string]> {
+      return this.entries()[Symbol.iterator]();
+    }
+
+    get [Symbol.toStringTag]() {
+      return "Headers";
+    }
+
+    [kNodeInspect]() {
+      return Object.fromEntries(this.entries());
+    }
+  })();
+
+export const NodeResHeadersProxy = /* @__PURE__ */ (() =>
+  class NodeResHeadersProxy implements Headers {
+    [kNodeRes]: ServerResponse;
+
+    constructor(res: ServerResponse) {
+      this[kNodeRes] = res;
+    }
+
+    append(name: string, value: string): void {
+      this[kNodeRes].appendHeader(name, value);
+    }
+
+    delete(name: string): void {
+      this[kNodeRes].removeHeader(name);
+    }
+
+    get(name: string): string | null {
+      return _normalizeValue(this[kNodeRes].getHeader(name));
+    }
+
+    getSetCookie(): string[] {
+      const setCookie = _normalizeValue(this[kNodeRes].getHeader("set-cookie"));
+      if (!setCookie) {
+        return [];
+      }
+      return splitCookiesString(setCookie);
+    }
+
+    has(name: string): boolean {
+      return this[kNodeRes].hasHeader(name);
+    }
+
+    set(name: string, value: string): void {
+      this[kNodeRes].setHeader(name, value);
+    }
+
+    forEach(
+      cb: (value: string, key: string, parent: Headers) => void,
+      thisArg?: any,
+    ): void {
+      const _headers = this[kNodeRes].getHeaders();
+      for (const key in _headers) {
+        if (_headers[key]) {
+          cb.call(thisArg, _normalizeValue(_headers[key]), key, this);
+        }
+      }
+    }
+
+    *entries(): IterableIterator<[string, string]> {
+      const _headers = this[kNodeRes].getHeaders();
+      for (const key in _headers) {
+        yield [key, _normalizeValue(_headers[key])];
+      }
+    }
+
+    *keys(): IterableIterator<string> {
+      const keys = this[kNodeRes].getHeaderNames();
+      for (const key of keys) {
+        yield key;
+      }
+    }
+
+    *values(): IterableIterator<string> {
+      const values = Object.values(this[kNodeRes].getHeaders());
+      for (const value of values) {
+        yield _normalizeValue(value);
+      }
+    }
+
+    [Symbol.iterator](): IterableIterator<[string, string]> {
+      return this.entries()[Symbol.iterator]();
+    }
+
+    get [Symbol.toStringTag]() {
+      return "Headers";
+    }
+
+    [kNodeInspect]() {
+      return Object.fromEntries(this.entries());
+    }
+  })();
+
+function _normalizeValue(
+  value: string | string[] | number | undefined,
+): string {
+  if (Array.isArray(value)) {
+    return value.join(", ");
+  }
+  return (value as string) || "";
+}
diff --git a/src/adapters/node/internal/request.ts b/src/adapters/node/internal/request.ts
new file mode 100644
index 0000000..4722e6a
--- /dev/null
+++ b/src/adapters/node/internal/request.ts
@@ -0,0 +1,194 @@
+import type { IncomingMessage } from "node:http";
+import { NodeReqHeadersProxy } from "./headers";
+import { kNodeInspect, kNodeReq } from "./utils";
+import { NodeReqURLProxy } from "./url";
+
+export const NodeRequestProxy = /* @__PURE__ */ (() =>
+  class NodeRequestProxy implements Request {
+    [kNodeReq]: IncomingMessage;
+
+    cache: RequestCache = "default";
+    credentials: RequestCredentials = "same-origin";
+    destination: RequestDestination = "";
+    integrity: string = "";
+    keepalive: boolean = false;
+
+    mode: RequestMode = "cors";
+    redirect: RequestRedirect = "follow";
+    referrer: string = "about:client";
+    referrerPolicy: ReferrerPolicy = "";
+
+    _url: URL;
+
+    headers: Headers;
+
+    _abortSignal?: AbortController;
+
+    bodyUsed: boolean = false;
+    __hasBody__: boolean | undefined;
+    _rawBody?: Promise<Uint8Array>;
+    _blobBody?: Promise<Blob>;
+    _formDataBody?: Promise<FormData>;
+    _jsonBody?: Promise<any>;
+    _textBody?: Promise<string>;
+    _bodyStream?: undefined | ReadableStream<Uint8Array>;
+
+    constructor(req: IncomingMessage) {
+      this[kNodeReq] = req;
+      this._url = new NodeReqURLProxy(req);
+      this.headers = new NodeReqHeadersProxy(this[kNodeReq]);
+    }
+
+    clone(): Request {
+      return new NodeRequestProxy(this[kNodeReq]);
+    }
+
+    get url() {
+      return this._url.href;
+    }
+
+    get method() {
+      return this[kNodeReq].method || "GET";
+    }
+
+    get signal() {
+      if (!this._abortSignal) {
+        this._abortSignal = new AbortController();
+      }
+      return this._abortSignal.signal;
+    }
+
+    get _hasBody() {
+      if (this.__hasBody__ !== undefined) {
+        return this.__hasBody__;
+      }
+      // Check if request method requires a payload
+      const method = this[kNodeReq].method?.toUpperCase();
+      if (
+        !method ||
+        !(
+          method === "PATCH" ||
+          method === "POST" ||
+          method === "PUT" ||
+          method === "DELETE"
+        )
+      ) {
+        this.__hasBody__ = false;
+        return false;
+      }
+
+      // Make sure either content-length or transfer-encoding/chunked is set
+      if (!Number.parseInt(this[kNodeReq].headers["content-length"] || "")) {
+        const isChunked = (this[kNodeReq].headers["transfer-encoding"] || "")
+          .split(",")
+          .map((e) => e.trim())
+          .filter(Boolean)
+          .includes("chunked");
+        if (!isChunked) {
+          this.__hasBody__ = false;
+          return false;
+        }
+      }
+      return true;
+    }
+
+    get body() {
+      if (!this._hasBody) {
+        return null;
+      }
+      if (!this._bodyStream) {
+        this.bodyUsed = true;
+        this._bodyStream = new ReadableStream({
+          start: (controller) => {
+            this[kNodeReq]
+              .on("data", (chunk) => {
+                controller.enqueue(chunk);
+              })
+              .once("error", (error) => {
+                controller.error(error);
+                this._abortSignal?.abort();
+              })
+              .once("close", () => {
+                this._abortSignal?.abort();
+              })
+              .once("end", () => {
+                controller.close();
+              });
+          },
+        });
+      }
+      return this._bodyStream;
+    }
+
+    arrayBuffer(): Promise<ArrayBuffer> {
+      if (!this._rawBody) {
+        const _bodyStream = this.body;
+        return _bodyStream
+          ? _readStream(_bodyStream)
+          : Promise.resolve(new ArrayBuffer(0));
+      }
+      return this._rawBody;
+    }
+
+    blob(): Promise<Blob> {
+      if (!this._blobBody) {
+        this._blobBody = this.arrayBuffer().then((buff) => {
+          return new Blob([buff], {
+            type: this[kNodeReq].headers["content-type"],
+          });
+        });
+      }
+      return this._blobBody;
+    }
+
+    formData(): Promise<FormData> {
+      if (!this._formDataBody) {
+        this._formDataBody = new Response(this.body, {
+          headers: this.headers,
+        }).formData();
+      }
+      return this._formDataBody;
+    }
+
+    json(): Promise<any> {
+      if (!this._jsonBody) {
+        this._jsonBody = this.text().then((txt) => {
+          return JSON.parse(txt);
+        });
+      }
+      return this._jsonBody;
+    }
+
+    text(): Promise<string> {
+      if (!this._textBody) {
+        this._textBody = this.arrayBuffer().then((buff) => {
+          return new TextDecoder().decode(buff);
+        });
+      }
+      return this._textBody;
+    }
+
+    get [Symbol.toStringTag]() {
+      return "Request";
+    }
+
+    [kNodeInspect]() {
+      return {
+        method: this.method,
+        url: this.url,
+        headers: this.headers,
+      };
+    }
+  })();
+
+async function _readStream(stream: ReadableStream) {
+  const chunks: Uint8Array[] = [];
+  await stream.pipeTo(
+    new WritableStream({
+      write(chunk) {
+        chunks.push(chunk);
+      },
+    }),
+  );
+  return Buffer.concat(chunks);
+}
diff --git a/src/adapters/node/internal/response.ts b/src/adapters/node/internal/response.ts
new file mode 100644
index 0000000..743375c
--- /dev/null
+++ b/src/adapters/node/internal/response.ts
@@ -0,0 +1,46 @@
+import type { ServerResponse } from "node:http";
+import type { H3Event } from "../../../types";
+import { kNodeInspect, kNodeRes } from "./utils";
+import { NodeResHeadersProxy } from "./headers";
+
+type H3Response = H3Event["response"];
+
+export const NodeResponseProxy = /* @__PURE__ */ (() =>
+  class NodeResponseProxy implements H3Response {
+    [kNodeRes]: ServerResponse;
+
+    headers: Headers;
+
+    constructor(res: ServerResponse) {
+      this[kNodeRes] = res;
+      this.headers = new NodeResHeadersProxy(res);
+    }
+
+    get status() {
+      return this[kNodeRes].statusCode;
+    }
+
+    set status(value) {
+      this[kNodeRes].statusCode = value;
+    }
+
+    get statusText() {
+      return this[kNodeRes].statusMessage;
+    }
+
+    set statusText(value) {
+      this[kNodeRes].statusMessage = value;
+    }
+
+    get [Symbol.toStringTag]() {
+      return "Response";
+    }
+
+    [kNodeInspect]() {
+      return {
+        status: this.status,
+        statusText: this.statusText,
+        headers: this.headers,
+      };
+    }
+  })();
diff --git a/src/adapters/node/internal/url.ts b/src/adapters/node/internal/url.ts
new file mode 100644
index 0000000..0155677
--- /dev/null
+++ b/src/adapters/node/internal/url.ts
@@ -0,0 +1,206 @@
+import type { IncomingMessage } from "node:http";
+import { kNodeInspect, kNodeReq } from "./utils";
+
+export const NodeReqURLProxy = /* @__PURE__ */ (() =>
+  class _NodeReqURLProxy implements URL {
+    [kNodeReq]: IncomingMessage;
+
+    _protocol?: string;
+    _hostname?: string;
+    _port?: string;
+
+    _pathname?: string;
+    _search?: string;
+    _searchParams?: URLSearchParams;
+
+    hash: string = "";
+    password: string = "";
+    username: string = "";
+
+    constructor(req: IncomingMessage) {
+      this[kNodeReq] = req;
+    }
+
+    // host
+    get host() {
+      return this[kNodeReq].headers.host || "";
+    }
+    set host(value: string) {
+      this._hostname = undefined;
+      this._port = undefined;
+      this[kNodeReq].headers.host = value;
+    }
+
+    // hostname
+    get hostname() {
+      if (this._hostname === undefined) {
+        const [hostname, port] = _parseHost(this[kNodeReq].headers.host);
+        if (this._port === undefined && port) {
+          this._port = String(Number.parseInt(port) || "");
+        }
+        this._hostname = hostname || "localhost";
+      }
+      return this._hostname;
+    }
+    set hostname(value: string) {
+      this._hostname = value;
+    }
+
+    // port
+    get port() {
+      if (this._port === undefined) {
+        const [hostname, port] = _parseHost(this[kNodeReq].headers.host);
+        if (this._hostname === undefined && hostname) {
+          this._hostname = hostname;
+        }
+        this._port = port || String(this[kNodeReq].socket?.localPort || "");
+      }
+      return this._port;
+    }
+    set port(value: string) {
+      this._port = String(Number.parseInt(value) || "");
+    }
+
+    // pathname
+    get pathname() {
+      if (this._pathname === undefined) {
+        const [pathname, search] = _parsePath(this[kNodeReq].url || "/");
+        this._pathname = pathname;
+        if (this._search === undefined) {
+          this._search = search;
+        }
+      }
+      return this._pathname;
+    }
+    set pathname(value: string) {
+      if (value[0] !== "/") {
+        value = "/" + value;
+      }
+      if (value === this._pathname) {
+        return;
+      }
+      this._pathname = value;
+      this[kNodeReq].url = value + this.search;
+    }
+
+    // search
+    get search() {
+      if (this._search === undefined) {
+        const [pathname, search] = _parsePath(this[kNodeReq].url || "/");
+        this._search = search;
+        if (this._pathname === undefined) {
+          this._pathname = pathname;
+        }
+      }
+      return this._search;
+    }
+    set search(value: string) {
+      if (value === "?") {
+        value = "";
+      } else if (value && value[0] !== "?") {
+        value = "?" + value;
+      }
+      if (value === this._search) {
+        return;
+      }
+      this._search = value;
+      this._searchParams = undefined;
+      this[kNodeReq].url = this.pathname + value;
+    }
+
+    // searchParams
+    get searchParams() {
+      if (!this._searchParams) {
+        this._searchParams = new URLSearchParams(this.search);
+      }
+      return this._searchParams;
+    }
+    set searchParams(value: URLSearchParams) {
+      this._searchParams = value;
+      this._search = value.toString();
+    }
+
+    // protocol
+    get protocol() {
+      if (!this._protocol) {
+        this._protocol =
+          (this[kNodeReq].socket as any)?.encrypted ||
+          this[kNodeReq].headers["x-forwarded-proto"] === "https"
+            ? "https:"
+            : "http:";
+      }
+      return this._protocol;
+    }
+    set protocol(value) {
+      this._protocol = value;
+    }
+
+    // origin
+    get origin() {
+      return `${this.protocol}//${this.host}`;
+    }
+    set origin(_value) {
+      // ignore
+    }
+
+    // href
+    get href() {
+      return `${this.protocol}//${this.host}${this.pathname}${this.search}`;
+    }
+    set href(value: string) {
+      const _url = new URL(value);
+      this._protocol = _url.protocol;
+      this.username = _url.username;
+      this.password = _url.password;
+      this._hostname = _url.hostname;
+      this._port = _url.port;
+      this.pathname = _url.pathname;
+      this.search = _url.search;
+      this.hash = _url.hash;
+    }
+
+    toString(): string {
+      return this.href;
+    }
+
+    toJSON(): string {
+      return this.href;
+    }
+
+    get [Symbol.toStringTag]() {
+      return "URL";
+    }
+
+    [kNodeInspect]() {
+      return this.href;
+    }
+  })();
+
+function _parsePath(input: string) {
+  let pIndex: number = -1;
+  let qIndex: number = -1;
+
+  const len = input.length;
+  for (let i = 0; i < len; i++) {
+    const c = input[i];
+    if (pIndex === -1 && c !== "/" && c !== "\\") {
+      pIndex = i;
+    }
+    if (qIndex === -1 && c === "?") {
+      qIndex = i;
+    }
+  }
+
+  const search = qIndex === -1 || qIndex >= len - 1 ? "" : input.slice(qIndex);
+  const path =
+    pIndex === -1
+      ? "/"
+      : "/" + input.slice(pIndex, qIndex === -1 ? undefined : qIndex);
+
+  return [path, search];
+}
+
+function _parseHost(host: string | undefined) {
+  const s = (host || "").split(":");
+  return [s[0], String(Number.parseInt(s[1]) || "")];
+}
diff --git a/src/adapters/node/_utils.ts b/src/adapters/node/internal/utils.ts
similarity index 79%
rename from src/adapters/node/_utils.ts
rename to src/adapters/node/internal/utils.ts
index 29d4b75..33e405a 100644
--- a/src/adapters/node/_utils.ts
+++ b/src/adapters/node/internal/utils.ts
@@ -1,21 +1,19 @@
+import type { IncomingMessage, ServerResponse } from "node:http";
 import type { Readable as NodeReadableStream } from "node:stream";
-import type {
-  NodeHandler,
-  NodeIncomingMessage,
-  NodeMiddleware,
-  NodeServerResponse,
-} from "../../types/node";
-import type { ResponseBody } from "../../types";
-import { _kRaw } from "../../event";
-import { createError } from "../../error";
-import { splitCookiesString } from "../../utils/cookie";
+import type { NodeHandler, NodeMiddleware } from "../../../types/node";
+import { createError } from "../../../error";
+import { splitCookiesString } from "../../../utils/cookie";
 import {
   sanitizeStatusCode,
   sanitizeStatusMessage,
-} from "../../utils/sanitize";
+} from "../../../utils/sanitize";
+
+export const kNodeReq: unique symbol = Symbol.for("h3.node.request");
+export const kNodeRes: unique symbol = Symbol.for("h3.node.response");
+export const kNodeInspect = Symbol.for("nodejs.util.inspect.custom");
 
 export function getBodyStream(
-  req: NodeIncomingMessage,
+  req: IncomingMessage,
 ): ReadableStream<Uint8Array> {
   return new ReadableStream({
     start(controller) {
@@ -33,8 +31,8 @@ export function getBodyStream(
 }
 
 export function sendNodeResponse(
-  nodeRes: NodeServerResponse,
-  handlerRes: ResponseBody,
+  nodeRes: ServerResponse,
+  handlerRes?: BodyInit | null,
 ): Promise<void> {
   // Web Response
   if (handlerRes instanceof Response) {
@@ -76,20 +74,22 @@ export function sendNodeResponse(
 
   // Node.js Readable Streams
   // https://nodejs.org/api/stream.html#readable-streams
-  if (typeof (handlerRes as NodeReadableStream)?.pipe === "function") {
+  if (
+    typeof (handlerRes as unknown as NodeReadableStream)?.pipe === "function"
+  ) {
     return new Promise<void>((resolve, reject) => {
       // Pipe stream to response
-      (handlerRes as NodeReadableStream).pipe(nodeRes);
+      (handlerRes as unknown as NodeReadableStream).pipe(nodeRes);
 
       // Handle stream events (if supported)
-      if ((handlerRes as NodeReadableStream).on) {
-        (handlerRes as NodeReadableStream).on("end", resolve);
-        (handlerRes as NodeReadableStream).on("error", reject);
+      if ((handlerRes as unknown as NodeReadableStream).on) {
+        (handlerRes as unknown as NodeReadableStream).on("end", resolve);
+        (handlerRes as unknown as NodeReadableStream).on("error", reject);
       }
 
       // Handle request aborts
       nodeRes.once("close", () => {
-        (handlerRes as NodeReadableStream).destroy?.();
+        (handlerRes as unknown as NodeReadableStream).destroy?.();
         // https://react.dev/reference/react-dom/server/renderToPipeableStream
         (handlerRes as any).abort?.();
       });
@@ -101,7 +101,7 @@ export function sendNodeResponse(
 }
 
 export function endNodeResponse(
-  res: NodeServerResponse,
+  res: ServerResponse,
   chunk?: any,
 ): Promise<void> {
   return new Promise((resolve) => {
@@ -124,8 +124,8 @@ export function normalizeHeaders(
 const payloadMethods = ["PATCH", "POST", "PUT", "DELETE"] as string[];
 
 export function readNodeReqBody(
-  req: NodeIncomingMessage,
-): undefined | Promise<Uint8Array | undefined> {
+  req: IncomingMessage,
+): undefined | Promise<Uint8Array> {
   // Check if request method requires a payload
   if (!req.method || !payloadMethods.includes(req.method?.toUpperCase())) {
     return;
@@ -161,8 +161,8 @@ export function readNodeReqBody(
 
 export function callNodeHandler(
   handler: NodeHandler | NodeMiddleware,
-  req: NodeIncomingMessage,
-  res: NodeServerResponse,
+  req: IncomingMessage,
+  res: ServerResponse,
 ) {
   const isMiddleware = handler.length > 2;
   return new Promise((resolve, reject) => {
diff --git a/src/adapters/web/_internal.ts b/src/adapters/web/_internal.ts
deleted file mode 100644
index 42fe7da..0000000
--- a/src/adapters/web/_internal.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import type { Readable as NodeReadableStream } from "node:stream";
-import type { ResponseBody } from "../../types";
-import { _kRaw } from "../../event";
-type WebNormalizedResponseBody = Exclude<ResponseBody, NodeReadableStream>;
-
-export function _normalizeResponse(
-  data: ResponseBody,
-): WebNormalizedResponseBody {
-  // Node.js Readable Streams
-  // https://nodejs.org/api/stream.html#readable-streams
-  if (typeof (data as NodeReadableStream)?.pipe === "function") {
-    // Convert to a ReadableStream
-    return new ReadableStream({
-      start(controller) {
-        (data as NodeReadableStream).on("data", (chunk) => {
-          controller.enqueue(chunk);
-        });
-        (data as NodeReadableStream).on("end", () => {
-          controller.close();
-        });
-      },
-    });
-  }
-  return data as WebNormalizedResponseBody;
-}
-
-export function _pathToRequestURL(path: string, headers?: HeadersInit): string {
-  if (path.startsWith("http://") || path.startsWith("https://")) {
-    return path;
-  }
-  const h = headers instanceof Headers ? headers : new Headers(headers);
-  const host = h.get("x-forwarded-host") || h.get("host") || "localhost";
-  const protocol = h.get("x-forwarded-proto") === "https" ? "https" : "http";
-  return `${protocol}://${host}${path}`;
-}
diff --git a/src/adapters/web/event.ts b/src/adapters/web/event.ts
index 9c0d8df..559ece1 100644
--- a/src/adapters/web/event.ts
+++ b/src/adapters/web/event.ts
@@ -1,167 +1,17 @@
-import type { RawEvent } from "../../types/event";
-import type { HTTPMethod } from "../../types";
-
-export class WebEvent implements RawEvent {
-  static isWeb = true;
-
-  _req: Request;
-
-  _path?: string;
-  _url?: URL;
-  _originalPath?: string | undefined;
-
-  _responseCode?: number;
-  _responseMessage?: string;
-  _responseHeaders: Headers = new Headers();
-
-  _rawBody?: Promise<undefined | Uint8Array>;
-  _textBody?: Promise<undefined | string>;
-  _formDataBody?: Promise<undefined | FormData>;
-
-  constructor(request: Request) {
-    this._req = request;
-  }
-
-  getContext() {
-    return {
-      request: this._req,
+import type { H3Event, H3EventContext } from "../../types";
+import { BaseEvent } from "../base/event";
+
+export class WebEvent extends BaseEvent implements H3Event {
+  request: Request;
+  url: URL;
+  response: H3Event["response"];
+
+  constructor(request: Request, context?: H3EventContext) {
+    super(context);
+    this.request = request;
+    this.url = new URL(request.url);
+    this.response = {
+      headers: new Headers(),
     };
   }
-
-  // -- request --
-
-  get method() {
-    return this._req.method as HTTPMethod;
-  }
-
-  get path() {
-    if (!this._path) {
-      if (!this._url) {
-        this._url = new URL(this._req.url);
-      }
-      this._path = this._originalPath = this._url.pathname + this._url.search;
-    }
-    return this._path;
-  }
-
-  set path(path: string) {
-    if (!this._originalPath) {
-      this._originalPath = this._path;
-    }
-    this._path = path;
-  }
-
-  get originalPath() {
-    return this._originalPath || this.path;
-  }
-
-  getHeader(key: string) {
-    return this._req.headers.get(key) ?? undefined;
-  }
-
-  getHeaders() {
-    return this._req.headers;
-  }
-
-  get remoteAddress() {
-    return undefined;
-  }
-
-  get isSecure() {
-    return undefined;
-  }
-
-  async readRawBody() {
-    if (!this._rawBody) {
-      this._rawBody = this._req
-        .arrayBuffer()
-        .then((buffer) => new Uint8Array(buffer));
-    }
-    return this._rawBody;
-  }
-
-  readTextBody() {
-    if (this._textBody) {
-      return this._textBody;
-    }
-    if (this._rawBody) {
-      this._textBody = Promise.resolve(this._rawBody).then((body) =>
-        body ? new TextDecoder().decode(body) : undefined,
-      );
-      return this._textBody;
-    }
-    this._textBody = this._req.text();
-    return this._textBody;
-  }
-
-  readFormDataBody() {
-    if (this._formDataBody) {
-      return this._formDataBody;
-    }
-    this._formDataBody = this._req.formData();
-    return this._formDataBody;
-  }
-
-  getBodyStream() {
-    return this._req.body || undefined;
-  }
-
-  // -- response --
-
-  get responseCode() {
-    return this._responseCode || 200;
-  }
-
-  set responseCode(code: number) {
-    this._responseCode = code;
-  }
-
-  get responseMessage() {
-    return this._responseMessage || "";
-  }
-
-  set responseMessage(message: string) {
-    this._responseMessage = message;
-  }
-
-  setResponseHeader(key: string, value: string) {
-    this._responseHeaders.set(key, value);
-  }
-
-  appendResponseHeader(key: string, value: string) {
-    this._responseHeaders.append(key, value);
-  }
-
-  getResponseHeader(key: string) {
-    return this._responseHeaders.get(key) ?? undefined;
-  }
-
-  getResponseHeaders() {
-    return this._responseHeaders;
-  }
-
-  getResponseSetCookie() {
-    if (this._responseHeaders.getSetCookie) {
-      return this._responseHeaders.getSetCookie();
-    }
-    const setCookie = this._responseHeaders.get("set-cookie");
-    return setCookie ? [setCookie] : [];
-  }
-
-  removeResponseHeader(key: string) {
-    this._responseHeaders.delete(key);
-  }
-
-  writeHead(code: number, message?: string) {
-    if (code) {
-      this.responseCode = code;
-    }
-    if (message) {
-      this.responseMessage = message;
-    }
-  }
-
-  writeEarlyHints(_hints: Record<string, string | string[]>) {
-    // noop
-  }
 }
diff --git a/src/adapters/web/index.ts b/src/adapters/web/index.ts
index 8b4d208..28353fc 100644
--- a/src/adapters/web/index.ts
+++ b/src/adapters/web/index.ts
@@ -1,49 +1,15 @@
-import type { H3, EventHandler, H3Event, H3EventContext } from "../../types";
-import { _kRaw } from "../../event";
-import { WebEvent } from "./event";
-import { _normalizeResponse, _pathToRequestURL } from "./_internal";
+import type { H3, EventHandler, H3EventContext } from "../../types";
 
-type WebHandler = (
-  request: Request,
-  context?: H3EventContext,
-) => Promise<Response>;
-
-export function toWebHandler(app: H3): WebHandler {
+export function toWebHandler(
+  app: H3,
+): (request: Request, context?: H3EventContext) => Promise<Response> {
   return (request, context) => {
-    return app.fetch(request, { h3: context });
+    return Promise.resolve(app.fetch(request, { h3: context }));
   };
 }
 
-export function fromWebHandler(handler: WebHandler): EventHandler {
-  return (event) => handler(toWebRequest(event), event.context);
-}
-
-/**
- * Convert an H3Event object to a web Request object.
- *
- */
-export function toWebRequest(event: H3Event): Request {
-  return (
-    (event[_kRaw] as WebEvent)._req ||
-    new Request(
-      _pathToRequestURL(event[_kRaw].path, event[_kRaw].getHeaders()),
-      {
-        // @ts-ignore Undici option
-        duplex: "half",
-        method: event[_kRaw].method,
-        headers: event[_kRaw].getHeaders(),
-        body: event[_kRaw].getBodyStream(),
-      },
-    )
-  );
-}
-
-export function getWebContext(
-  event: H3Event,
-): undefined | ReturnType<WebEvent["getContext"]> {
-  const raw = event[_kRaw] as WebEvent;
-  if (!(raw?.constructor as any)?.isWeb) {
-    return undefined;
-  }
-  return raw.getContext();
+export function fromWebHandler(
+  handler: (request: Request, context?: H3EventContext) => Promise<Response>,
+): EventHandler {
+  return (event) => handler(event.request, event.context);
 }
diff --git a/src/error.ts b/src/error.ts
index 784c25a..af9be29 100644
--- a/src/error.ts
+++ b/src/error.ts
@@ -1,4 +1,3 @@
-import { _kRaw } from "./event";
 import { hasProp } from "./utils/internal/object";
 import { sanitizeStatusMessage, sanitizeStatusCode } from "./utils/sanitize";
 
diff --git a/src/event.ts b/src/event.ts
deleted file mode 100644
index d24efbc..0000000
--- a/src/event.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-import { WebEvent } from "./adapters/web/event";
-import type { H3EventContext, H3Event } from "./types";
-import { RawEvent } from "./types/event";
-
-export const _kRaw: unique symbol = Symbol.for("h3.rawEvent");
-
-export class EventWrapper implements H3Event {
-  static "__is_event__" = true;
-
-  context = Object.create(null);
-
-  [_kRaw]: RawEvent;
-
-  constructor(raw: RawEvent, initialContext?: H3EventContext) {
-    this[_kRaw] = raw;
-    if (initialContext) {
-      Object.assign(this.context, initialContext);
-    }
-  }
-
-  get method() {
-    return this[_kRaw].method || "GET";
-  }
-
-  get path() {
-    return this[_kRaw].path;
-  }
-
-  get headers(): Headers {
-    return this[_kRaw].getHeaders();
-  }
-
-  toString() {
-    return `[${this.method}] ${this.path}`;
-  }
-
-  toJSON() {
-    return this.toString();
-  }
-}
-
-/**
- * Checks if the input is an H3Event object.
- * @param input - The input to check.
- * @returns True if the input is an H3Event object, false otherwise.
- * @see H3Event
- */
-export function isEvent(input: any): input is H3Event {
-  const ctor = input?.constructor;
-  return (
-    ctor.__is_event__ ||
-    input.__is_event__ /* Backward compatibility with h3 v1 */
-  );
-}
-
-export function mockEvent(
-  _request: string | URL | Request,
-  options?: RequestInit & { h3?: H3EventContext },
-) {
-  let request: Request;
-  if (typeof _request === "string") {
-    let url = _request;
-    if (url[0] === "/") {
-      url = `http://localhost${url}`;
-    }
-    request = new Request(url, options);
-  } else if (options || _request instanceof URL) {
-    request = new Request(_request, options);
-  } else {
-    request = _request;
-  }
-  const webEvent = new WebEvent(request);
-  return new EventWrapper(webEvent, options?.h3);
-}
diff --git a/src/h3.ts b/src/h3.ts
index 04cc840..2a56d0e 100644
--- a/src/h3.ts
+++ b/src/h3.ts
@@ -15,12 +15,10 @@ import {
   RouterContext,
   findRoute,
 } from "rou3";
-import { _kRaw, EventWrapper } from "./event";
 import { getPathname, joinURL } from "./utils/internal/path";
 import { ResolvedEventHandler } from "./types/handler";
 import { WebEvent } from "./adapters/web/event";
-import { _kNotFound, prepareResponse } from "./response";
-import { _normalizeResponse } from "./adapters/web/_internal";
+import { kNotFound, prepareResponse } from "./response";
 
 /**
  * Create a new h3 instance.
@@ -36,7 +34,7 @@ class _H3 implements H3 {
   _mRouter?: RouterContext<H3Route>;
   _router?: RouterContext<H3Route>;
 
-  handler: EventHandler<EventHandlerRequest, Promise<unknown>>;
+  handler: EventHandler<EventHandlerRequest, unknown | Promise<unknown>>;
 
   constructor(config: H3Config) {
     this.config = config;
@@ -63,10 +61,10 @@ class _H3 implements H3 {
     };
   }
 
-  async fetch(
+  fetch(
     _request: Request | URL | string,
     options?: RequestInit & { h3?: H3EventContext },
-  ): Promise<Response> {
+  ): Response | Promise<Response> {
     // Normalize request
     let request: Request;
     if (typeof _request === "string") {
@@ -82,71 +80,101 @@ class _H3 implements H3 {
     }
 
     // Create event context
-    const rawEvent = new WebEvent(request);
-    const event = new EventWrapper(rawEvent, options?.h3);
+    const event = new WebEvent(request, options?.h3);
 
-    // Handle request
-    const _res = await this._handler(event)
-      .catch((error: any) => error)
-      .then((res) => prepareResponse(event, res, this.config));
+    let _handlerRes: unknown | Promise<unknown>;
+    try {
+      _handlerRes = this._handler(event);
+    } catch (error: any) {
+      _handlerRes = error;
+    }
 
-    // Create response
-    const status = rawEvent.responseCode;
-    // prettier-ignore
-    // https://developer.mozilla.org/en-US/docs/Web/API/Response/body
-    const isNullBody = status === 101 || status === 204 || status === 205 || status === 304 || request.method === "HEAD";
-    return new Response(isNullBody ? null : _normalizeResponse(_res), {
-      status,
-      statusText: rawEvent.responseMessage,
-      headers: rawEvent.getResponseHeaders(),
-    });
+    if (_handlerRes instanceof Promise) {
+      return _handlerRes.then((_resolvedRes) => {
+        const _body = prepareResponse(event, _resolvedRes, this.config);
+        return _body instanceof Promise
+          ? _body.then((body) => new Response(body, event.response))
+          : new Response(_body, event.response);
+      });
+    }
+
+    const _body = prepareResponse(event, _handlerRes, this.config);
+    return _body instanceof Promise
+      ? _body.then((body) => new Response(body, event.response))
+      : new Response(_body, event.response);
   }
 
-  async _handler(event: H3Event) {
-    const _path = event.path;
-    const _queryIndex = _path.indexOf("?");
-    const pathname = _queryIndex === -1 ? _path : _path.slice(0, _queryIndex);
+  _handler(event: H3Event) {
+    const pathname = event.url.pathname;
+
+    let _chain: Promise<unknown> | undefined;
 
     // 1. Hooks
     if (this.config.onRequest) {
-      await this.config.onRequest(event);
+      _chain = Promise.resolve(this.config.onRequest(event));
     }
 
     // 2. Global middleware
     const _middleware = this._middleware;
     if (_middleware) {
-      for (const entry of _middleware) {
-        const result = await entry.handler(event);
-        if (result !== undefined && result !== _kNotFound) {
-          return result;
+      _chain = (_chain || Promise.resolve()).then(async () => {
+        for (const entry of _middleware) {
+          const result = await entry.handler(event);
+          if (result !== undefined && result !== kNotFound) {
+            return result;
+          }
         }
-      }
+      });
     }
 
     // 3. Middleware router
     const _mRouter = this._mRouter;
     if (_mRouter) {
-      const matches = findAllRoutes(_mRouter, event.method, pathname);
-      for (const match of matches) {
-        const result = await match.data.handler(event);
-        if (result !== undefined && result !== _kNotFound) {
-          return result;
+      _chain = (_chain || Promise.resolve()).then(async (_previous) => {
+        if (_previous !== undefined && _previous !== kNotFound) {
+          return _previous;
         }
-      }
+        const matches = findAllRoutes(_mRouter, event.request.method, pathname);
+        for (const match of matches) {
+          const result = await match.data.handler(event);
+          if (result !== undefined && result !== kNotFound) {
+            return result;
+          }
+        }
+      });
     }
 
     // 4. Route handler
     if (this._router) {
-      const match = findRoute(this._router, event.method, pathname)?.[0];
+      const match = findRoute(
+        this._router,
+        event.request.method,
+        pathname,
+      )?.[0];
       if (match) {
-        event.context.params = match.params;
-        event.context.matchedRoute = match.data;
-        return match.data.handler(event);
+        if (_chain) {
+          return _chain.then((_previous) => {
+            if (_previous !== undefined && _previous !== kNotFound) {
+              return _previous;
+            }
+            event.context.params = match.params;
+            event.context.matchedRoute = match.data;
+            return match.data.handler(event);
+          });
+        } else {
+          event.context.params = match.params;
+          event.context.matchedRoute = match.data;
+          return match.data.handler(event);
+        }
       }
     }
 
     // 5. 404
-    return _kNotFound;
+    return _chain
+      ? _chain.then((_previous) =>
+          _previous === undefined ? kNotFound : _previous,
+        )
+      : kNotFound;
   }
 
   async resolve(
diff --git a/src/handler.ts b/src/handler.ts
index e3b73f0..ab0c3b1 100644
--- a/src/handler.ts
+++ b/src/handler.ts
@@ -10,7 +10,6 @@ import type {
   EventHandlerResponse,
   EventHandlerObject,
 } from "./types";
-import { _kRaw } from "./event";
 
 type _EventHandlerHooks<
   Request extends EventHandlerRequest = EventHandlerRequest,
diff --git a/src/index.ts b/src/index.ts
index e8e4bee..e6fe1c8 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -5,7 +5,7 @@ export * from "./types";
 export { createH3 } from "./h3";
 
 // Event
-export { isEvent, mockEvent } from "./event";
+export { isEvent, mockEvent } from "./utils/event";
 
 // Handler
 export {
@@ -23,7 +23,6 @@ export { createError, isError } from "./error";
 
 // Node
 export {
-  getNodeContext,
   fromNodeHandler,
   toNodeHandler,
   defineNodeHandler,
@@ -31,26 +30,18 @@ export {
 } from "./adapters/node";
 
 // Web
-export {
-  getWebContext,
-  fromWebHandler,
-  toWebHandler,
-  toWebRequest,
-} from "./adapters/web";
+export { fromWebHandler, toWebHandler } from "./adapters/web";
 
 // ------ Utils ------
 
 // Request
 export {
-  getRequestHeader,
-  getRequestHeaders,
   getRequestHost,
   getRequestIP,
   getRequestProtocol,
   getRequestURL,
   isMethod,
   getQuery,
-  getMethod,
   getValidatedQuery,
   assertMethod,
   getRouterParam,
@@ -60,19 +51,7 @@ export {
 
 // Response
 export {
-  appendResponseHeader,
-  appendResponseHeaders,
-  setResponseHeader,
-  setResponseHeaders,
-  setResponseStatus,
   writeEarlyHints,
-  removeResponseHeader,
-  clearResponseHeaders,
-  defaultContentType,
-  getResponseHeader,
-  getResponseHeaders,
-  getResponseStatus,
-  getResponseStatusText,
   redirect,
   iterable,
   noContent,
@@ -87,14 +66,7 @@ export {
 } from "./utils/proxy";
 
 // Body
-export {
-  readRawBody,
-  getBodyStream,
-  readFormDataBody,
-  readJSONBody,
-  readTextBody,
-  readValidatedJSONBody,
-} from "./utils/body";
+export { readBody, readValidatedBody } from "./utils/body";
 
 // Cookie
 export {
diff --git a/src/response.ts b/src/response.ts
index 2e5f881..7e35ff5 100644
--- a/src/response.ts
+++ b/src/response.ts
@@ -1,66 +1,85 @@
-import type { H3Config, H3Event, ResponseBody } from "./types";
+import type { H3Config, H3Event } from "./types";
 import type { H3Response, H3Error } from "./types/h3";
 import { createError } from "./error";
 import { isJSONSerializable } from "./utils/internal/object";
 import { MIMES } from "./utils/internal/consts";
-import { _kRaw } from "./event";
 
-export const _kNotFound = Symbol.for("h3.notFound");
+export const kNotFound = Symbol.for("h3.notFound");
 
-export async function prepareResponse(
+export function prepareResponse(
   event: H3Event,
   body: unknown,
   config: H3Config,
-) {
-  const res = await normalizeResponseBody(event, body, config);
+): BodyInit | null | undefined | Promise<BodyInit | null | undefined> {
+  const res: H3Response = bodyToRes(event, body, config);
+
+  const status = event.response.status;
+  if (
+    status === 100 ||
+    status === 101 ||
+    status === 102 ||
+    status === 204 ||
+    status === 205 ||
+    status === 304 ||
+    event.method === "HEAD"
+  ) {
+    res.body = null;
+  }
+
+  if (res.contentType && !event.response.headers.has("content-type")) {
+    event.response.headers.set("content-type", res.contentType);
+  }
+
+  if (res.headers) {
+    for (const [key, value] of res.headers.entries()) {
+      event.response.headers.set(key, value);
+    }
+  }
+
+  if (res.status) {
+    event.response.status = res.status;
+  }
+
+  if (res.statusText) {
+    event.response.statusText = res.statusText;
+  }
+
+  let promise: Promise<unknown> | undefined;
+
   if (res.error) {
     if (res.error.unhandled) {
       console.error("[h3] Unhandled Error:", res.error);
     }
     if (config.onError) {
       try {
-        await config.onError(res.error, event);
+        promise = Promise.resolve(config.onError(res.error, event));
       } catch (hookError) {
         console.error("[h3] Error while calling `onError` hook:", hookError);
       }
     }
   }
+
   if (config.onBeforeResponse) {
-    await config.onBeforeResponse(event, res);
-  }
-  if (res.contentType && !event[_kRaw].getResponseHeader("content-type")) {
-    event[_kRaw].setResponseHeader("content-type", res.contentType);
-  }
-  if (res.headers) {
-    for (const [key, value] of res.headers.entries()) {
-      event[_kRaw].setResponseHeader(key, value);
-    }
-  }
-  if (res.status) {
-    event[_kRaw].responseCode = res.status;
-  }
-  if (res.statusText) {
-    event[_kRaw].responseMessage = res.statusText;
+    promise = (promise || Promise.resolve()).then(() =>
+      config.onBeforeResponse!(event, res),
+    );
   }
-  return res.body;
+
+  return promise ? promise.then(() => res.body) : res.body;
 }
 
-function normalizeResponseBody(
-  event: H3Event,
-  val: unknown,
-  config: H3Config,
-): H3Response | Promise<H3Response> {
+function bodyToRes(event: H3Event, val: unknown, config: H3Config): H3Response {
   // Empty Content
   if (val === null || val === undefined) {
     return { body: "" };
   }
 
   // Not found
-  if (val === _kNotFound) {
-    return errorToH3Response(
+  if (val === kNotFound) {
+    return errorToRes(
       {
         statusCode: 404,
-        statusMessage: `Cannot find any route matching [${event.method}] ${event.path}`,
+        statusMessage: `Cannot find any route matching [${event.request.method}] ${event.path}`,
       },
       config,
     );
@@ -80,7 +99,7 @@ function normalizeResponseBody(
 
   // Error (should be before JSON)
   if (val instanceof Error) {
-    return errorToH3Response(val, config);
+    return errorToRes(val, config);
   }
 
   // JSON
@@ -108,17 +127,15 @@ function normalizeResponseBody(
 
   // Blob
   if (val instanceof Blob) {
-    return val.arrayBuffer().then((arrayBuffer) => {
-      return {
-        contentType: val.type,
-        body: new Uint8Array(arrayBuffer),
-      };
-    });
+    return {
+      contentType: val.type,
+      body: val.stream(),
+    };
   }
 
   // Symbol or Function is not supported
   if (valType === "symbol" || valType === "function") {
-    return errorToH3Response(
+    return errorToRes(
       {
         statusCode: 500,
         statusMessage: `[h3] Cannot send ${valType} as response.`,
@@ -128,11 +145,11 @@ function normalizeResponseBody(
   }
 
   return {
-    body: val as ResponseBody,
+    body: val as BodyInit,
   };
 }
 
-export function errorToH3Response(
+export function errorToRes(
   _error: Partial<H3Error> | Error,
   config: H3Config,
 ): H3Response {
diff --git a/src/types/event.ts b/src/types/event.ts
index b556f3c..b9dfa66 100644
--- a/src/types/event.ts
+++ b/src/types/event.ts
@@ -1,59 +1,26 @@
+import type { IncomingMessage, ServerResponse } from "node:http";
 import type { EventHandlerRequest, H3EventContext, HTTPMethod } from ".";
-import type { _kRaw } from "../event";
-
-type MaybePromise<T> = T | Promise<T>;
 
 export interface H3Event<
   _RequestT extends EventHandlerRequest = EventHandlerRequest,
 > {
-  // Internal raw context
-  [_kRaw]: RawEvent;
-
   // Context
-  context: H3EventContext;
+  readonly context: H3EventContext;
+
+  // Platform specific
+  node?: {
+    req: IncomingMessage;
+    res: ServerResponse;
+  };
 
   // Request
   readonly method: HTTPMethod;
   readonly path: string;
+  readonly url: URL;
   readonly headers: Headers;
+  readonly request: Request;
+  readonly ip?: string | undefined;
 
-  toString(): string;
-  toJSON(): string;
-}
-
-export interface RawEvent {
-  // -- Context --
-  getContext: () => Record<string, unknown>;
-
-  // -- Request --
-
-  path: string;
-  readonly originalPath: string;
-  readonly method: HTTPMethod;
-
-  readonly remoteAddress?: string | undefined;
-  readonly isSecure?: boolean | undefined;
-
-  getHeader: (key: string) => string | undefined;
-  getHeaders: () => Headers;
-
-  readRawBody: () => MaybePromise<Uint8Array | undefined>;
-  readTextBody: () => MaybePromise<string | undefined>;
-  readFormDataBody: () => MaybePromise<FormData | undefined>;
-  getBodyStream: () => ReadableStream<Uint8Array> | undefined;
-
-  // -- Response --
-
-  responseCode: number | undefined;
-  responseMessage: string | undefined;
-
-  setResponseHeader: (key: string, value: string) => void;
-  appendResponseHeader: (key: string, value: string) => void;
-  getResponseHeader: (key: string) => string | undefined;
-  getResponseHeaders: () => Headers;
-  getResponseSetCookie: () => string[];
-  removeResponseHeader: (key: string) => void;
-  writeHead: (code: number, message?: string) => void;
-  // sendResponse: (data?: RawResponse) => void | Promise<void>;
-  writeEarlyHints: (hints: Record<string, string>) => void | Promise<void>;
+  // Response
+  response: { headers: Headers; status?: number; statusText?: string };
 }
diff --git a/src/types/h3.ts b/src/types/h3.ts
index 523f73f..f26e5d6 100644
--- a/src/types/h3.ts
+++ b/src/types/h3.ts
@@ -4,7 +4,6 @@ import type {
   EventHandler,
   EventHandlerRequest,
   ResolvedEventHandler,
-  ResponseBody,
 } from "./handler";
 import type { H3Error } from "../error";
 import type { HTTPMethod } from "./http";
@@ -12,11 +11,9 @@ import { H3EventContext } from "./context";
 
 export type { H3Error } from "../error";
 
-type MaybePromise<T> = T | Promise<T>;
-
 export interface H3Response {
   error?: H3Error;
-  body: ResponseBody;
+  body?: BodyInit | null;
   contentType?: string;
   headers?: Headers;
   status?: number;
@@ -53,6 +50,8 @@ export interface H3Route {
 
 type AddRoute = (route: string, handler: EventHandler | H3) => H3;
 
+type MaybePromise<T = unknown> = T | Promise<T>;
+
 export interface H3 {
   readonly config: H3Config;
 
@@ -63,11 +62,11 @@ export interface H3 {
   fetch(
     request: Request | URL | string,
     options?: RequestInit & { h3?: H3EventContext },
-  ): Promise<Response>;
+  ): Response | Promise<Response>;
 
   /** main event handler */
-  handler: EventHandler<EventHandlerRequest, Promise<unknown>>;
-  _handler: EventHandler<EventHandlerRequest, Promise<unknown>>;
+  handler: EventHandler<EventHandlerRequest, MaybePromise<unknown>>;
+  _handler: EventHandler<EventHandlerRequest, MaybePromise<unknown>>;
 
   /** resolve event handler */
   resolve: (
diff --git a/src/types/handler.ts b/src/types/handler.ts
index 0189752..a475215 100644
--- a/src/types/handler.ts
+++ b/src/types/handler.ts
@@ -1,21 +1,13 @@
-import type { Readable as NodeReadableStream } from "node:stream";
-import type { QueryObject } from "ufo";
 import type { H3Event } from "./event";
 import type { Hooks as WSHooks } from "crossws";
 import type { HTTPMethod } from "./http";
 import type { H3 } from "./h3";
 
-export type ResponseBody =
-  | undefined // middleware pass
-  | null // empty content
-  | BodyInit
-  | NodeReadableStream;
-
 export type EventHandlerResponse<T = unknown> = T | Promise<T>;
 
 export interface EventHandlerRequest {
   body?: unknown;
-  query?: QueryObject;
+  query?: Record<string, string>;
   routerParams?: Record<string, string>;
 }
 
diff --git a/src/types/index.ts b/src/types/index.ts
index c2d1f31..b9073eb 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -12,7 +12,6 @@ export type {
   EventHandlerResolver,
   EventHandlerResponse,
   DynamicEventHandler,
-  ResponseBody,
   LazyEventHandler,
   InferEventInput,
   RequestMiddleware,
diff --git a/src/types/node.ts b/src/types/node.ts
index 6288792..eeeb3b4 100644
--- a/src/types/node.ts
+++ b/src/types/node.ts
@@ -1,22 +1,14 @@
-import type {
-  IncomingMessage as NodeIncomingMessage,
-  ServerResponse as NodeServerResponse,
-} from "node:http";
-
-export type {
-  IncomingMessage as NodeIncomingMessage,
-  ServerResponse as NodeServerResponse,
-} from "node:http";
+import type { IncomingMessage, ServerResponse } from "node:http";
 
 export type NodeHandler = (
-  req: NodeIncomingMessage,
-  res: NodeServerResponse,
+  req: IncomingMessage,
+  res: ServerResponse,
 ) => unknown | Promise<unknown>;
 
 export type NodeMiddleware = (
-  req: NodeIncomingMessage,
-  res: NodeServerResponse,
+  req: IncomingMessage,
+  res: ServerResponse,
   next: (error?: Error) => void,
 ) => unknown | Promise<unknown>;
 
-export type { NodeEvent } from "../adapters/node/_event";
+export type { NodeEvent } from "../adapters/node/event";
diff --git a/src/types/utils/session.ts b/src/types/utils/session.ts
index d016aa1..ac6e282 100644
--- a/src/types/utils/session.ts
+++ b/src/types/utils/session.ts
@@ -1,6 +1,6 @@
 import type { CookieSerializeOptions } from "cookie-es";
 import type { SealOptions } from "iron-webcrypto";
-import type { _kGetSession } from "../../utils/internal/session";
+import type { kGetSession } from "../../utils/internal/session";
 
 type SessionDataT = Record<string, any>;
 
@@ -10,7 +10,7 @@ export interface Session<T extends SessionDataT = SessionDataT> {
   id: string;
   createdAt: number;
   data: SessionData<T>;
-  [_kGetSession]?: Promise<Session<T>>;
+  [kGetSession]?: Promise<Session<T>>;
 }
 
 export interface SessionConfig {
diff --git a/src/types/utils/static.ts b/src/types/utils/static.ts
index b478c9b..dfd1ff8 100644
--- a/src/types/utils/static.ts
+++ b/src/types/utils/static.ts
@@ -1,5 +1,3 @@
-import type { ResponseBody } from "../handler";
-
 export interface StaticAssetMeta {
   type?: string;
   etag?: string;
@@ -20,7 +18,9 @@ export interface ServeStaticOptions {
   /**
    * This function should resolve asset content
    */
-  getContents: (id: string) => ResponseBody | Promise<ResponseBody>;
+  getContents: (
+    id: string,
+  ) => BodyInit | null | undefined | Promise<BodyInit | null | undefined>;
 
   /**
    * Map of supported encodings (compressions) and their file extensions.
diff --git a/src/utils/base.ts b/src/utils/base.ts
index c037c7c..800bf8c 100644
--- a/src/utils/base.ts
+++ b/src/utils/base.ts
@@ -1,4 +1,3 @@
-import { _kRaw } from "../event";
 import { H3, EventHandler } from "../types";
 import { withoutBase, withoutTrailingSlash } from "./internal/path";
 
@@ -20,10 +19,10 @@ export function withBase(base: string, input: EventHandler | H3): EventHandler {
   const _originalHandler = (input as H3)?.handler || (input as EventHandler);
 
   const _handler: EventHandler = async (event) => {
-    const _pathBefore = event[_kRaw].path || "/";
-    event[_kRaw].path = withoutBase(event.path || "/", base);
+    const _pathBefore = event.url.pathname || "/";
+    event.url.pathname = withoutBase(event.path || "/", base);
     return Promise.resolve(_originalHandler(event)).finally(() => {
-      event[_kRaw].path = _pathBefore;
+      event.url.pathname = _pathBefore;
     });
   };
 
diff --git a/src/utils/body.ts b/src/utils/body.ts
index fedb7f3..92e2391 100644
--- a/src/utils/body.ts
+++ b/src/utils/body.ts
@@ -1,51 +1,14 @@
 import type { InferEventInput, ValidateFunction, H3Event } from "../types";
-import { _kRaw } from "../event";
 import { createError } from "../error";
 import { validateData } from "./internal/validate";
 import { parseURLEncodedBody } from "./internal/body";
 
-/**
- * Reads body of the request and returns an Uint8Array of the raw body.
- *
- * @example
- * app.use("/", async (event) => {
- *   const body = await readRawBody(event);
- * });
- *
- * @param event {H3Event} H3 event or req passed by h3 handler
- *
- * @return {Uint8Array} Raw body
- */
-export async function readRawBody(
-  event: H3Event,
-): Promise<Uint8Array | undefined> {
-  return await event[_kRaw].readRawBody();
-}
-
-/**
- * Reads body of the request and returns an string (utf-8) of the raw body.
- *
- * @example
- * app.use("/", async (event) => {
- *   const body = await readTextBody(event);
- * });
- *
- * @param event {H3Event} H3 event or req passed by h3 handler
- *
- * @return {string} Text body
- */
-export async function readTextBody(
-  event: H3Event,
-): Promise<string | undefined> {
-  return await event[_kRaw].readTextBody();
-}
-
 /**
  * Reads request body and tries to parse using JSON.parse or URLSearchParams.
  *
  * @example
  * app.use("/", async (event) => {
- *   const body = await readAndParseBody(event);
+ *   const body = await readBody(event);
  * });
  *
  * @param event H3 event passed by h3 handler
@@ -53,17 +16,17 @@ export async function readTextBody(
  *
  * @return {*} The `Object`, `Array`, `String`, `Number`, `Boolean`, or `null` value corresponding to the request JSON body
  */
-export async function readJSONBody<
+export async function readBody<
   T,
   _Event extends H3Event = H3Event,
   _T = InferEventInput<"body", _Event, T>,
 >(event: _Event): Promise<undefined | _T> {
-  const text = await event[_kRaw].readTextBody();
+  const text = await event.request.text();
   if (!text) {
     return undefined;
   }
 
-  const contentType = event[_kRaw].getHeader("content-type") || "";
+  const contentType = event.request.headers.get("content-type") || "";
   if (contentType.startsWith("application/x-www-form-urlencoded")) {
     return parseURLEncodedBody(text) as _T;
   }
@@ -80,13 +43,13 @@ export async function readJSONBody<
 }
 
 /**
- * Tries to read the request body via `readJSONBody`, then uses the provided validation function and either throws a validation error or returns the result.
+ * Tries to read the request body via `readBody`, then uses the provided validation function and either throws a validation error or returns the result.
  *
  * You can use a simple function to validate the body or use a library like `zod` to define a schema.
  *
  * @example
  * app.use("/", async (event) => {
- *   const body = await readValidatedJSONBody(event, (body) => {
+ *   const body = await readValidatedBody(event, (body) => {
  *     return typeof body === "object" && body !== null;
  *   });
  * });
@@ -95,49 +58,20 @@ export async function readJSONBody<
  *
  * app.use("/", async (event) => {
  *   const objectSchema = z.object();
- *   const body = await readValidatedJSONBody(event, objectSchema.safeParse);
+ *   const body = await readValidatedBody(event, objectSchema.safeParse);
  * });
  *
  * @param event The H3Event passed by the handler.
  * @param validate The function to use for body validation. It will be called passing the read request body. If the result is not false, the parsed body will be returned.
  * @throws If the validation function returns `false` or throws, a validation error will be thrown.
  * @return {*} The `Object`, `Array`, `String`, `Number`, `Boolean`, or `null` value corresponding to the request JSON body.
- * @see {readJSONBody}
+ * @see {readBody}
  */
-export async function readValidatedJSONBody<
+export async function readValidatedBody<
   T,
   Event extends H3Event = H3Event,
   _T = InferEventInput<"body", Event, T>,
 >(event: Event, validate: ValidateFunction<_T>): Promise<_T> {
-  const _body = await readJSONBody(event);
+  const _body = await readBody(event);
   return validateData(_body, validate);
 }
-
-/**
- * Constructs a FormData object from an event, after converting it to a a web request.
- *
- * @example
- * app.use("/", async (event) => {
- *   const formData = await readFormDataBody(event);
- *   const email = formData.get("email");
- *   const password = formData.get("password");
- * });
- *
- * @param event The H3Event object to read the form data from.
- */
-export async function readFormDataBody(
-  event: H3Event,
-): Promise<FormData | undefined> {
-  return await event[_kRaw].readFormDataBody();
-}
-
-/**
- * Captures a stream from a request.
- * @param event The H3Event object containing the request information.
- * @returns Undefined if the request can't transport a payload, otherwise a ReadableStream of the request body.
- */
-export function getBodyStream(
-  event: H3Event,
-): undefined | ReadableStream<Uint8Array> {
-  return event[_kRaw].getBodyStream();
-}
diff --git a/src/utils/cache.ts b/src/utils/cache.ts
index 601f19e..a52c23f 100644
--- a/src/utils/cache.ts
+++ b/src/utils/cache.ts
@@ -1,5 +1,4 @@
 import type { CacheConditions, H3Event } from "../types";
-import { _kRaw } from "../event";
 
 /**
  * Check request caching headers (`If-Modified-Since`) and add caching headers (Last-Modified, Cache-Control)
@@ -19,25 +18,25 @@ export function handleCacheHeaders(
 
   if (opts.modifiedTime) {
     const modifiedTime = new Date(opts.modifiedTime);
-    const ifModifiedSince = event[_kRaw].getHeader("if-modified-since");
-    event[_kRaw].setResponseHeader("last-modified", modifiedTime.toUTCString());
+    const ifModifiedSince = event.request.headers.get("if-modified-since");
+    event.response.headers.set("last-modified", modifiedTime.toUTCString());
     if (ifModifiedSince && new Date(ifModifiedSince) >= opts.modifiedTime) {
       cacheMatched = true;
     }
   }
 
   if (opts.etag) {
-    event[_kRaw].setResponseHeader("etag", opts.etag);
-    const ifNonMatch = event[_kRaw].getHeader("if-none-match");
+    event.response.headers.set("etag", opts.etag);
+    const ifNonMatch = event.request.headers.get("if-none-match");
     if (ifNonMatch === opts.etag) {
       cacheMatched = true;
     }
   }
 
-  event[_kRaw].setResponseHeader("cache-control", cacheControls.join(", "));
+  event.response.headers.set("cache-control", cacheControls.join(", "));
 
   if (cacheMatched) {
-    event[_kRaw].responseCode = 304;
+    event.response.status = 304;
     return true;
   }
 
diff --git a/src/utils/cookie.ts b/src/utils/cookie.ts
index 4717dae..3779bc6 100644
--- a/src/utils/cookie.ts
+++ b/src/utils/cookie.ts
@@ -1,7 +1,6 @@
 import type { CookieSerializeOptions } from "cookie-es";
 import type { H3Event } from "../types";
 import { parse as parseCookie, serialize as serializeCookie } from "cookie-es";
-import { _kRaw } from "../event";
 
 /**
  * Parse the request to get HTTP Cookie header string and returning an object of all cookie name-value pairs.
@@ -12,7 +11,7 @@ import { _kRaw } from "../event";
  * ```
  */
 export function parseCookies(event: H3Event): Record<string, string> {
-  return parseCookie(event[_kRaw].getHeader("cookie") || "");
+  return parseCookie(event.request.headers.get("cookie") || "");
 }
 
 /**
@@ -48,15 +47,15 @@ export function setCookie(
   const newCookie = serializeCookie(name, value, { path: "/", ...options });
 
   // Check and add only not any other set-cookie headers already set
-  const currentCookies = event[_kRaw].getResponseSetCookie();
+  const currentCookies = event.response.headers.getSetCookie();
   if (currentCookies.length === 0) {
-    event[_kRaw].setResponseHeader("set-cookie", newCookie);
+    event.response.headers.set("set-cookie", newCookie);
     return;
   }
 
   // Merge and deduplicate unique set-cookie headers
   const newCookieKey = _getDistinctCookieKey(name, options || {});
-  event[_kRaw].removeResponseHeader("set-cookie");
+  event.response.headers.delete("set-cookie");
   for (const cookie of currentCookies) {
     const _key = _getDistinctCookieKey(
       cookie.split("=")?.[0],
@@ -65,9 +64,9 @@ export function setCookie(
     if (_key === newCookieKey) {
       continue;
     }
-    event[_kRaw].appendResponseHeader("set-cookie", cookie);
+    event.response.headers.append("set-cookie", cookie);
   }
-  event[_kRaw].appendResponseHeader("set-cookie", newCookie);
+  event.response.headers.append("set-cookie", newCookie);
 }
 
 /**
diff --git a/src/utils/cors.ts b/src/utils/cors.ts
index 96832cd..d5e5bdc 100644
--- a/src/utils/cors.ts
+++ b/src/utils/cors.ts
@@ -1,7 +1,6 @@
-import type { H3Event, ResponseBody } from "../types";
+import type { H3Event } from "../types";
 import type { H3CorsOptions } from "../types/utils/cors";
-import { _kRaw } from "../event";
-import { noContent, appendResponseHeaders } from "./response";
+import { noContent } from "./response";
 import {
   createAllowHeaderHeaders,
   createCredentialsHeaders,
@@ -17,12 +16,16 @@ export { isCorsOriginAllowed } from "./internal/cors";
  * Check if the incoming request is a CORS preflight request.
  */
 export function isPreflightRequest(event: H3Event): boolean {
-  const origin = event[_kRaw].getHeader("origin");
-  const accessControlRequestMethod = event[_kRaw].getHeader(
+  const origin = event.request.headers.get("origin");
+  const accessControlRequestMethod = event.request.headers.get(
     "access-control-request-method",
   );
 
-  return event.method === "OPTIONS" && !!origin && !!accessControlRequestMethod;
+  return (
+    event.request.method === "OPTIONS" &&
+    !!origin &&
+    !!accessControlRequestMethod
+  );
 }
 
 /**
@@ -32,20 +35,30 @@ export function appendCorsPreflightHeaders(
   event: H3Event,
   options: H3CorsOptions,
 ) {
-  appendResponseHeaders(event, createOriginHeaders(event, options));
-  appendResponseHeaders(event, createCredentialsHeaders(options));
-  appendResponseHeaders(event, createExposeHeaders(options));
-  appendResponseHeaders(event, createMethodsHeaders(options));
-  appendResponseHeaders(event, createAllowHeaderHeaders(event, options));
+  const headers = {
+    ...createOriginHeaders(event, options),
+    ...createCredentialsHeaders(options),
+    ...createExposeHeaders(options),
+    ...createMethodsHeaders(options),
+    ...createAllowHeaderHeaders(event, options),
+  };
+  for (const [key, value] of Object.entries(headers)) {
+    event.response.headers.append(key, value);
+  }
 }
 
 /**
  * Append CORS headers to the response.
  */
 export function appendCorsHeaders(event: H3Event, options: H3CorsOptions) {
-  appendResponseHeaders(event, createOriginHeaders(event, options));
-  appendResponseHeaders(event, createCredentialsHeaders(options));
-  appendResponseHeaders(event, createExposeHeaders(options));
+  const headers = {
+    ...createOriginHeaders(event, options),
+    ...createCredentialsHeaders(options),
+    ...createExposeHeaders(options),
+  };
+  for (const [key, value] of Object.entries(headers)) {
+    event.response.headers.append(key, value);
+  }
 }
 
 /**
@@ -73,10 +86,7 @@ export function appendCorsHeaders(event: H3Event, options: H3CorsOptions) {
  *  });
  * );
  */
-export function handleCors(
-  event: H3Event,
-  options: H3CorsOptions,
-): false | ResponseBody {
+export function handleCors(event: H3Event, options: H3CorsOptions): false | "" {
   const _options = resolveCorsOptions(options);
   if (isPreflightRequest(event)) {
     appendCorsPreflightHeaders(event, options);
diff --git a/src/utils/event-stream.ts b/src/utils/event-stream.ts
index 4fd00d0..0a1b5dc 100644
--- a/src/utils/event-stream.ts
+++ b/src/utils/event-stream.ts
@@ -1,6 +1,5 @@
 import type { H3Event } from "../types";
 import type { EventStreamOptions } from "../types/utils/sse";
-import { _kRaw } from "../event";
 import { EventStream } from "./internal/event-stream";
 
 /**
diff --git a/src/utils/event.ts b/src/utils/event.ts
new file mode 100644
index 0000000..05864d1
--- /dev/null
+++ b/src/utils/event.ts
@@ -0,0 +1,35 @@
+import { WebEvent } from "../adapters/web/event";
+import type { H3EventContext, H3Event } from "../types";
+
+/**
+ * Checks if the input is an H3Event object.
+ * @param input - The input to check.
+ * @returns True if the input is an H3Event object, false otherwise.
+ * @see H3Event
+ */
+export function isEvent(input: any): input is H3Event {
+  const ctor = input?.constructor;
+  return (
+    ctor.__is_event__ ||
+    input.__is_event__ /* Backward compatibility with h3 v1 */
+  );
+}
+
+export function mockEvent(
+  _request: string | URL | Request,
+  options?: RequestInit & { h3?: H3EventContext },
+): H3Event {
+  let request: Request;
+  if (typeof _request === "string") {
+    let url = _request;
+    if (url[0] === "/") {
+      url = `http://localhost${url}`;
+    }
+    request = new Request(url, options);
+  } else if (options || _request instanceof URL) {
+    request = new Request(_request, options);
+  } else {
+    request = _request;
+  }
+  return new WebEvent(request);
+}
diff --git a/src/utils/fingerprint.ts b/src/utils/fingerprint.ts
index 7ffeeb4..aecfd26 100644
--- a/src/utils/fingerprint.ts
+++ b/src/utils/fingerprint.ts
@@ -1,6 +1,5 @@
 import type { H3Event, RequestFingerprintOptions } from "../types";
 import crypto from "uncrypto";
-import { _kRaw } from "../event";
 import { getRequestIP } from "./request";
 
 /**
@@ -22,7 +21,7 @@ export async function getRequestFingerprint(
   }
 
   if (opts.method === true) {
-    fingerprint.push(event.method);
+    fingerprint.push(event.request.method);
   }
 
   if (opts.path === true) {
@@ -30,7 +29,7 @@ export async function getRequestFingerprint(
   }
 
   if (opts.userAgent === true) {
-    fingerprint.push(event[_kRaw].getHeader("user-agent"));
+    fingerprint.push(event.request.headers.get("user-agent"));
   }
 
   const fingerprintString = fingerprint.filter(Boolean).join("|");
diff --git a/src/utils/internal/cors.ts b/src/utils/internal/cors.ts
index 84c2640..4648e44 100644
--- a/src/utils/internal/cors.ts
+++ b/src/utils/internal/cors.ts
@@ -9,7 +9,6 @@ import type {
   H3AccessControlExposeHeadersHeader,
   H3AccessControlMaxAgeHeader,
 } from "../../types/utils/cors";
-import { _kRaw } from "../../event";
 
 /**
  * Resolve CORS options.
@@ -78,7 +77,7 @@ export function createOriginHeaders(
   options: H3CorsOptions,
 ): H3AccessControlAllowOriginHeader {
   const { origin: originOption } = options;
-  const origin = event[_kRaw].getHeader("origin");
+  const origin = event.request.headers.get("origin");
 
   if (!origin || !originOption || originOption === "*") {
     return { "access-control-allow-origin": "*" };
@@ -139,7 +138,7 @@ export function createAllowHeaderHeaders(
   const { allowHeaders } = options;
 
   if (!allowHeaders || allowHeaders === "*" || allowHeaders.length === 0) {
-    const header = event[_kRaw].getHeader("access-control-request-headers");
+    const header = event.request.headers.get("access-control-request-headers");
 
     return header
       ? {
diff --git a/src/utils/internal/event-stream.ts b/src/utils/internal/event-stream.ts
index d88cc54..dde1025 100644
--- a/src/utils/internal/event-stream.ts
+++ b/src/utils/internal/event-stream.ts
@@ -1,14 +1,8 @@
-import type { H3Event, ResponseBody } from "../../types";
-import type { ResponseHeaders } from "../../types/http";
-import type { NodeEvent } from "../../types/node";
+import type { H3Event } from "../../types";
 import type {
   EventStreamMessage,
   EventStreamOptions,
 } from "../../types/utils/sse";
-import { getRequestHeader } from "../request";
-import { setResponseHeaders } from "../response";
-import { _kRaw } from "../../event";
-import { setResponseStatus } from "../response";
 
 /**
  * A helper class for [server sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format)
@@ -32,7 +26,7 @@ export class EventStream {
       this._writerIsClosed = true;
     });
     if (opts.autoclose !== false) {
-      (this._event[_kRaw] as NodeEvent)._res?.once("close", () => this.close());
+      this._event.node?.res.once("close", () => this.close());
     }
   }
 
@@ -150,9 +144,9 @@ export class EventStream {
     this._writer.closed.then(cb);
   }
 
-  async send(): Promise<ResponseBody> {
+  async send(): Promise<BodyInit> {
     setEventStreamHeaders(this._event);
-    setResponseStatus(this._event, 200);
+    this._event.response.status = 200;
     this._handled = true;
     return this._transformStream.readable;
   }
@@ -191,23 +185,21 @@ export function formatEventStreamMessages(
 }
 
 export function setEventStreamHeaders(event: H3Event) {
-  const headers: ResponseHeaders = {
-    "Content-Type": "text/event-stream",
-    "Cache-Control":
-      "private, no-cache, no-store, no-transform, must-revalidate, max-age=0",
-    "X-Accel-Buffering": "no", // prevent nginx from buffering the response
-  };
+  event.response.headers.set("content-type", "text/event-stream");
+  event.response.headers.set(
+    "cache-control",
+    "private, no-cache, no-store, no-transform, must-revalidate, max-age=0",
+  );
+  // prevent nginx from buffering the response
+  event.response.headers.set("x-accel-buffering", "no");
 
   if (!isHttp2Request(event)) {
-    headers.Connection = "keep-alive";
+    event.response.headers.set("connection", "keep-alive");
   }
-
-  setResponseHeaders(event, headers);
 }
 
 export function isHttp2Request(event: H3Event) {
   return (
-    getRequestHeader(event, ":path") !== undefined &&
-    getRequestHeader(event, ":method") !== undefined
+    event.request.headers.has(":path") || event.request.headers.has(":method")
   );
 }
diff --git a/src/utils/internal/session.ts b/src/utils/internal/session.ts
index 41ddb00..bafa5e1 100644
--- a/src/utils/internal/session.ts
+++ b/src/utils/internal/session.ts
@@ -1,6 +1,6 @@
 import type { SessionConfig } from "../../types";
 
-export const _kGetSession: unique symbol = Symbol.for(
+export const kGetSession: unique symbol = Symbol.for(
   "h3.internal.session.promise",
 );
 
diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts
index ae9937d..c1f899f 100644
--- a/src/utils/proxy.ts
+++ b/src/utils/proxy.ts
@@ -1,13 +1,6 @@
-import type {
-  H3EventContext,
-  H3Event,
-  ProxyOptions,
-  Duplex,
-  ResponseBody,
-} from "../types";
+import type { H3EventContext, H3Event, ProxyOptions, Duplex } from "../types";
 import { splitCookiesString } from "./cookie";
 import { sanitizeStatusMessage, sanitizeStatusCode } from "./sanitize";
-import { _kRaw } from "../event";
 import { createError } from "../error";
 import {
   PayloadMethods,
@@ -28,17 +21,17 @@ export async function proxyRequest(
   // Request Body
   let body;
   let duplex: Duplex | undefined;
-  if (PayloadMethods.has(event.method)) {
+  if (PayloadMethods.has(event.request.method)) {
     if (opts.streamRequest) {
-      body = event[_kRaw].getBodyStream();
+      body = event.request.body;
       duplex = "half";
     } else {
-      body = await event[_kRaw].readRawBody();
+      body = await event.request.arrayBuffer();
     }
   }
 
   // Method
-  const method = opts.fetchOptions?.method || event.method;
+  const method = opts.fetchOptions?.method || event.request.method;
 
   // Headers
   const fetchHeaders = mergeHeaders(
@@ -66,7 +59,7 @@ export async function proxy(
   event: H3Event,
   target: string,
   opts: ProxyOptions = {},
-): Promise<ResponseBody> {
+): Promise<BodyInit | undefined | null> {
   let response: Response | undefined;
   try {
     response = await getFetch(opts.fetch)(target, {
@@ -81,11 +74,11 @@ export async function proxy(
       cause: error,
     });
   }
-  event[_kRaw].responseCode = sanitizeStatusCode(
+  event.response.status = sanitizeStatusCode(
     response.status,
-    event[_kRaw].responseCode,
+    event.response.status,
   );
-  event[_kRaw].responseMessage = sanitizeStatusMessage(response.statusText);
+  event.response.statusText = sanitizeStatusMessage(response.statusText);
 
   const cookies: string[] = [];
 
@@ -100,7 +93,7 @@ export async function proxy(
       cookies.push(...splitCookiesString(value));
       continue;
     }
-    event[_kRaw].setResponseHeader(key, value);
+    event.response.headers.set(key, value);
   }
 
   if (cookies.length > 0) {
@@ -118,7 +111,7 @@ export async function proxy(
       return cookie;
     });
     for (const cookie of _cookies) {
-      event[_kRaw].appendResponseHeader("set-cookie", cookie);
+      event.response.headers.append("set-cookie", cookie);
     }
   }
 
@@ -145,7 +138,7 @@ export async function proxy(
  */
 export function getProxyRequestHeaders(event: H3Event) {
   const headers = Object.create(null);
-  for (const [name, value] of event[_kRaw].getHeaders()) {
+  for (const [name, value] of event.request.headers.entries()) {
     if (!ignoredHeaders.has(name)) {
       headers[name] = value;
     }
diff --git a/src/utils/request.ts b/src/utils/request.ts
index 111cd0d..77178dd 100644
--- a/src/utils/request.ts
+++ b/src/utils/request.ts
@@ -1,17 +1,14 @@
-import { getQuery as _getQuery, decode as decodeURI } from "ufo";
 import { createError } from "../error";
 import type {
   HTTPMethod,
   InferEventInput,
-  RequestHeaders,
   ValidateFunction,
   H3Event,
 } from "../types";
-import { _kRaw } from "../event";
 import { validateData } from "./internal/validate";
 
 /**
- * Get query the params object from the request URL parsed with [unjs/ufo](https://ufo.unjs.io).
+ * Get query the params object from the request URL.
  *
  * @example
  * app.use("/", (event) => {
@@ -23,11 +20,11 @@ export function getQuery<
   Event extends H3Event = H3Event,
   _T = Exclude<InferEventInput<"query", Event, T>, undefined>,
 >(event: Event): _T {
-  return _getQuery(event.path || "") as _T;
+  return Object.fromEntries(event.url.searchParams.entries()) as _T;
 }
 
 /**
- * Get the query param from the request URL parsed with [unjs/ufo](https://ufo.unjs.io) and validated with validate function.
+ * Get the query param from the request URL validated with validate function.
  *
  * You can use a simple function to validate the query object or a library like `zod` to define a schema.
  *
@@ -61,7 +58,7 @@ export function getValidatedQuery<
 /**
  * Get matched route params.
  *
- * If `decode` option is `true`, it will decode the matched route params using `decodeURI`.
+ * If `decode` option is `true`, it will decode the matched route params using `decodeURIComponent`.
  *
  * @example
  * app.use("/", (event) => {
@@ -77,10 +74,9 @@ export function getRouterParams(
   if (opts.decode) {
     params = { ...params };
     for (const key in params) {
-      params[key] = decodeURI(params[key]);
+      params[key] = decodeURIComponent(params[key]);
     }
   }
-
   return params;
 }
 
@@ -119,7 +115,6 @@ export function getValidatedRouterParams<
   opts: { decode?: boolean } = {},
 ): Promise<_T> {
   const routerParams = getRouterParams(event, opts);
-
   return validateData(routerParams, validate);
 }
 
@@ -139,20 +134,9 @@ export function getRouterParam(
   opts: { decode?: boolean } = {},
 ): string | undefined {
   const params = getRouterParams(event, opts);
-
   return params[name];
 }
 
-/**
- * @deprecated Directly use `event.method` instead.
- */
-export function getMethod(
-  event: H3Event,
-  defaultMethod: HTTPMethod = "GET",
-): HTTPMethod {
-  return (event.method || defaultMethod).toUpperCase() as HTTPMethod;
-}
-
 /**
  *
  * Checks if the incoming request method is of the expected type.
@@ -173,15 +157,15 @@ export function isMethod(
   expected: HTTPMethod | HTTPMethod[],
   allowHead?: boolean,
 ) {
-  if (allowHead && event.method === "HEAD") {
+  if (allowHead && event.request.method === "HEAD") {
     return true;
   }
 
   if (typeof expected === "string") {
-    if (event.method === expected) {
+    if (event.request.method === expected) {
       return true;
     }
-  } else if (expected.includes(event.method)) {
+  } else if (expected.includes(event.request.method as HTTPMethod)) {
     return true;
   }
 
@@ -214,36 +198,6 @@ export function assertMethod(
   }
 }
 
-/**
- * Get the request headers object.
- *
- * Array headers are joined with a comma.
- *
- * @example
- * app.use("/", (event) => {
- *   const headers = getRequestHeaders(event); // { "content-type": "application/json", "x-custom-header": "value" }
- * });
- */
-export function getRequestHeaders(event: H3Event): RequestHeaders {
-  return Object.fromEntries(event[_kRaw].getHeaders().entries());
-}
-
-/**
- * Get a request header by name.
- *
- * @example
- * app.use("/", (event) => {
- *   const contentType = getRequestHeader(event, "content-type"); // "application/json"
- * });
- */
-export function getRequestHeader(
-  event: H3Event,
-  name: keyof RequestHeaders,
-): RequestHeaders[typeof name] | undefined {
-  const value = event[_kRaw].getHeader(name.toLowerCase());
-  return value || undefined;
-}
-
 /**
  * Get the request hostname.
  *
@@ -261,12 +215,12 @@ export function getRequestHost(
   opts: { xForwardedHost?: boolean } = {},
 ) {
   if (opts.xForwardedHost) {
-    const xForwardedHost = event[_kRaw].getHeader("x-forwarded-host");
+    const xForwardedHost = event.request.headers.get("x-forwarded-host");
     if (xForwardedHost) {
       return xForwardedHost;
     }
   }
-  return event[_kRaw].getHeader("host") || "localhost";
+  return event.request.headers.get("host") || "localhost";
 }
 
 /**
@@ -287,11 +241,11 @@ export function getRequestProtocol(
 ) {
   if (
     opts.xForwardedProto !== false &&
-    event[_kRaw].getHeader("x-forwarded-proto") === "https"
+    event.request.headers.get("x-forwarded-proto") === "https"
   ) {
     return "https";
   }
-  return event[_kRaw].isSecure ? "https" : "http";
+  return event.url.protocol.slice(0, -1);
 }
 
 /**
@@ -310,12 +264,12 @@ export function getRequestURL(
   event: H3Event,
   opts: { xForwardedHost?: boolean; xForwardedProto?: boolean } = {},
 ) {
+  if (opts.xForwardedHost === undefined && opts.xForwardedProto === undefined) {
+    return event.url;
+  }
   const host = getRequestHost(event, opts);
   const protocol = getRequestProtocol(event, opts);
-  const path = (event[_kRaw].originalPath || event[_kRaw].path).replace(
-    /^[/\\]+/g,
-    "/",
-  );
+  const path = event.url.pathname + event.url.search;
   return new URL(path, `${protocol}://${host}`);
 }
 
@@ -342,21 +296,14 @@ export function getRequestIP(
     xForwardedFor?: boolean;
   } = {},
 ): string | undefined {
-  if (event.context.clientAddress) {
-    return event.context.clientAddress;
-  }
-
   if (opts.xForwardedFor) {
     // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#syntax
-    const _header = event[_kRaw].getHeader("x-forwarded-for");
-    const xForwardedFor = (Array.isArray(_header) ? _header[0] : _header)
-      ?.split(",")
-      .shift()
-      ?.trim();
+    const _header = event.request.headers.get("x-forwarded-for");
+    const xForwardedFor = (_header || "")?.split(",").shift()?.trim();
     if (xForwardedFor) {
       return xForwardedFor;
     }
   }
 
-  return event[_kRaw].remoteAddress;
+  return event.context.clientAddress || event.ip || undefined;
 }
diff --git a/src/utils/response.ts b/src/utils/response.ts
index 5e39dac..44908a1 100644
--- a/src/utils/response.ts
+++ b/src/utils/response.ts
@@ -1,13 +1,5 @@
-import type {
-  H3Event,
-  ResponseHeaders,
-  ResponseHeaderName,
-  MimeType,
-  StatusCode,
-} from "../types";
-import { _kRaw } from "../event";
-import { MIMES } from "./internal/consts";
-import { sanitizeStatusCode, sanitizeStatusMessage } from "./sanitize";
+import type { H3Event, StatusCode } from "../types";
+import { sanitizeStatusCode } from "./sanitize";
 import {
   serializeIterableValue,
   coerceIterable,
@@ -24,83 +16,22 @@ import {
  * @param event H3 event
  * @param code status code to be send. By default, it is `204 No Content`.
  */
-export function noContent(event: H3Event, code?: StatusCode) {
-  if (!code && event[_kRaw].responseCode !== 200) {
-    // status code was set with setResponseStatus
-    code = event[_kRaw].responseCode;
-  }
-  const _code = sanitizeStatusCode(code, 204);
-  // 204 responses MUST NOT have a Content-Length header field
-  // https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2
-  if (_code === 204) {
-    event[_kRaw].removeResponseHeader("content-length");
-  }
-  event[_kRaw].writeHead(_code);
-  return "";
-}
+export function noContent(event: H3Event, code?: StatusCode): "" {
+  const currentStatus = event.response.status;
 
-/**
- * Set the response status code and message.
- *
- * @example
- * app.use("/", (event) => {
- *   setResponseStatus(event, 404, "Not Found");
- *   return "Not Found";
- * });
- */
-export function setResponseStatus(
-  event: H3Event,
-  code?: StatusCode,
-  text?: string,
-): void {
-  if (code) {
-    event[_kRaw].responseCode = sanitizeStatusCode(
-      code,
-      event[_kRaw].responseCode,
-    );
-  }
-  if (text) {
-    event[_kRaw].responseMessage = sanitizeStatusMessage(text);
+  if (!code && currentStatus && currentStatus !== 200) {
+    code = event.response.status;
   }
-}
 
-/**
- * Get the current response status code.
- *
- * @example
- * app.use("/", (event) => {
- *   const status = getResponseStatus(event);
- *   return `Status: ${status}`;
- * });
- */
-export function getResponseStatus(event: H3Event): number {
-  return event[_kRaw].responseCode || 200;
-}
+  event.response.status = sanitizeStatusCode(code, 204);
 
-/**
- * Get the current response status message.
- *
- * @example
- * app.use("/", (event) => {
- *   const statusText = getResponseStatusText(event);
- *   return `Status: ${statusText}`;
- * });
- */
-export function getResponseStatusText(event: H3Event): string {
-  return event[_kRaw].responseMessage || "";
-}
-
-/**
- * Set the response status code and message.
- */
-export function defaultContentType(event: H3Event, type?: MimeType) {
-  if (
-    type &&
-    event[_kRaw].responseCode !== 304 /* unjs/h3#603 */ &&
-    !event[_kRaw].getResponseHeader("content-type")
-  ) {
-    event[_kRaw].setResponseHeader("content-type", type);
+  // 204 responses MUST NOT have a Content-Length header field
+  // https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2
+  if (event.response.status === 204) {
+    event.response.headers.delete("content-length");
   }
+
+  return "";
 }
 
 /**
@@ -125,157 +56,14 @@ export function redirect(
   location: string,
   code: StatusCode = 302,
 ) {
-  event[_kRaw].responseCode = sanitizeStatusCode(
-    code,
-    event[_kRaw].responseCode,
-  );
-  event[_kRaw].setResponseHeader("location", location);
+  event.response.status = sanitizeStatusCode(code, event.response.status);
+  event.response.headers.set("location", location);
   const encodedLoc = location.replace(/"/g, "%22");
   const html = `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${encodedLoc}"></head></html>`;
-  defaultContentType(event, MIMES.html);
-  return html;
-}
-
-/**
- * Get the response headers object.
- *
- * @example
- * app.use("/", (event) => {
- *   const headers = getResponseHeaders(event);
- * });
- */
-export function getResponseHeaders(event: H3Event) {
-  return event[_kRaw].getResponseHeaders();
-}
-
-export function getResponseHeader(event: H3Event, name: string) {
-  return event[_kRaw].getResponseHeader(name);
-}
-
-/**
- * Set the response headers.
- *
- * @example
- * app.use("/", (event) => {
- *   setResponseHeaders(event, {
- *     "content-type": "text/html",
- *     "cache-control": "no-cache",
- *   });
- * });
- */
-export function setResponseHeaders(
-  event: H3Event,
-  headers: ResponseHeaders,
-): void {
-  for (const [name, value] of Object.entries(headers)) {
-    event[_kRaw].setResponseHeader(name, value!);
-  }
-}
-
-/**
- * Set a response header by name.
- *
- * @example
- * app.use("/", (event) => {
- *   setResponseHeader(event, "content-type", "text/html");
- * });
- */
-export function setResponseHeader<T extends keyof ResponseHeaders>(
-  event: H3Event,
-  name: T,
-  value: ResponseHeaders[T] | ResponseHeaders[T][],
-): void {
-  if (Array.isArray(value)) {
-    event[_kRaw].removeResponseHeader(name);
-    for (const valueItem of value) {
-      event[_kRaw].appendResponseHeader(name, valueItem!);
-    }
-  } else {
-    event[_kRaw].setResponseHeader(name, value!);
-  }
-}
-
-/**
- * Append the response headers.
- *
- * @example
- * app.use("/", (event) => {
- *   appendResponseHeaders(event, {
- *     "content-type": "text/html",
- *     "cache-control": "no-cache",
- *   });
- * });
- */
-export function appendResponseHeaders(
-  event: H3Event,
-  headers: ResponseHeaders,
-): void {
-  for (const [name, value] of Object.entries(headers)) {
-    appendResponseHeader(event, name, value!);
+  if (!event.response.headers.has("content-type")) {
+    event.response.headers.set("content-type", "text/html");
   }
-}
-
-/**
- * Append a response header by name.
- *
- * @example
- * app.use("/", (event) => {
- *   appendResponseHeader(event, "content-type", "text/html");
- * });
- */
-export function appendResponseHeader<T extends string>(
-  event: H3Event,
-  name: T,
-  value: ResponseHeaders[T] | ResponseHeaders[T][],
-): void {
-  if (Array.isArray(value)) {
-    for (const valueItem of value) {
-      event[_kRaw].appendResponseHeader(name, valueItem!);
-    }
-  } else {
-    event[_kRaw].appendResponseHeader(name, value!);
-  }
-}
-
-/**
- * Remove all response headers, or only those specified in the headerNames array.
- *
- * @example
- * app.use("/", (event) => {
- *   clearResponseHeaders(event, ["content-type", "cache-control"]); // Remove content-type and cache-control headers
- * });
- *
- * @param event H3 event
- * @param headerNames Array of header names to remove
- */
-export function clearResponseHeaders(
-  event: H3Event,
-  headerNames?: ResponseHeaderName[],
-): void {
-  if (headerNames && headerNames.length > 0) {
-    for (const name of headerNames) {
-      event[_kRaw].removeResponseHeader(name);
-    }
-  } else {
-    for (const name of event[_kRaw].getResponseHeaders().keys()) {
-      event[_kRaw].removeResponseHeader(name);
-    }
-  }
-}
-
-/**
- * Remove a response header by name.
- *
- * @example
- * app.use("/", (event) => {
- *   removeResponseHeader(event, "content-type"); // Remove content-type header
- * });
- */
-export function removeResponseHeader(
-  event: H3Event,
-  name: ResponseHeaderName,
-): void {
-  return event[_kRaw].removeResponseHeader(name);
+  return html;
 }
 
 /**
@@ -285,7 +73,12 @@ export function writeEarlyHints(
   event: H3Event,
   hints: Record<string, string>,
 ): void | Promise<void> {
-  return event[_kRaw].writeEarlyHints(hints);
+  if (!event.node?.res?.writeEarlyHints) {
+    return;
+  }
+  return new Promise((resolve) => {
+    event.node?.res.writeEarlyHints(hints, () => resolve());
+  });
 }
 
 /**
@@ -302,7 +95,7 @@ export function writeEarlyHints(
  * @template Value - Test
  *
  * @example
- * return iterable(async function* work() {
+ * return iterable(event, async function* work() {
  *   // Open document body
  *   yield "<!DOCTYPE html>\n<html><body><h1>Executing...</h1><ol>\n";
  *   // Do work ...
@@ -321,6 +114,7 @@ export function writeEarlyHints(
  * }
  */
 export function iterable<Value = unknown, Return = unknown>(
+  _event: H3Event,
   iterable: IterationSource<Value, Return>,
   options?: {
     serializer: IteratorSerializer<Value | Return>;
diff --git a/src/utils/session.ts b/src/utils/session.ts
index 276c8fc..f649bc9 100644
--- a/src/utils/session.ts
+++ b/src/utils/session.ts
@@ -1,12 +1,11 @@
 import type { H3Event, Session, SessionConfig, SessionData } from "../types";
 import crypto from "uncrypto";
 import { seal, unseal, defaults as sealDefaults } from "iron-webcrypto";
-import { _kRaw } from "../event";
 import { getCookie, setCookie } from "./cookie";
 import {
   DEFAULT_SESSION_NAME,
   DEFAULT_SESSION_COOKIE,
-  _kGetSession,
+  kGetSession,
 } from "./internal/session";
 
 /**
@@ -55,7 +54,7 @@ export async function getSession<T extends SessionData = SessionData>(
   // Wait for existing session to load
   const existingSession = event.context.sessions![sessionName] as Session<T>;
   if (existingSession) {
-    return existingSession[_kGetSession] || existingSession;
+    return existingSession[kGetSession] || existingSession;
   }
 
   // Prepare an empty session object and store in context
@@ -74,7 +73,7 @@ export async function getSession<T extends SessionData = SessionData>(
       typeof config.sessionHeader === "string"
         ? config.sessionHeader.toLowerCase()
         : `x-${sessionName.toLowerCase()}-session`;
-    const headerValue = event[_kRaw].getHeader(headerName);
+    const headerValue = event.request.headers.get(headerName);
     if (typeof headerValue === "string") {
       sealedSession = headerValue;
     }
@@ -89,10 +88,10 @@ export async function getSession<T extends SessionData = SessionData>(
       .catch(() => {})
       .then((unsealed) => {
         Object.assign(session, unsealed);
-        delete event.context.sessions![sessionName][_kGetSession];
+        delete event.context.sessions![sessionName][kGetSession];
         return session as Session<T>;
       });
-    event.context.sessions![sessionName][_kGetSession] = promise;
+    event.context.sessions![sessionName][kGetSession] = promise;
     await promise;
   }
 
diff --git a/src/utils/static.ts b/src/utils/static.ts
index ae14d4b..3aca8fe 100644
--- a/src/utils/static.ts
+++ b/src/utils/static.ts
@@ -1,11 +1,4 @@
-import type {
-  H3Event,
-  StaticAssetMeta,
-  ServeStaticOptions,
-  ResponseBody,
-} from "../types";
-import { decodePath } from "ufo";
-import { _kRaw } from "../event";
+import type { H3Event, StaticAssetMeta, ServeStaticOptions } from "../types";
 import { createError } from "../error";
 import {
   withLeadingSlash,
@@ -19,8 +12,8 @@ import {
 export async function serveStatic(
   event: H3Event,
   options: ServeStaticOptions,
-): Promise<false | ResponseBody> {
-  if (event.method !== "GET" && event.method !== "HEAD") {
+): Promise<false | undefined | null | BodyInit> {
+  if (event.request.method !== "GET" && event.request.method !== "HEAD") {
     if (!options.fallthrough) {
       throw createError({
         statusMessage: "Method Not Allowed",
@@ -30,17 +23,17 @@ export async function serveStatic(
     return false;
   }
 
-  const originalId = decodePath(
+  const originalId = decodeURI(
     withLeadingSlash(withoutTrailingSlash(getPathname(event.path))),
   );
 
   const acceptEncodings = parseAcceptEncoding(
-    event[_kRaw].getHeader("accept-encoding") || "",
+    event.request.headers.get("accept-encoding") || "",
     options.encodings,
   );
 
   if (acceptEncodings.length > 1) {
-    event[_kRaw].setResponseHeader("vary", "accept-encoding");
+    event.response.headers.set("vary", "accept-encoding");
   }
 
   let id = originalId;
@@ -71,50 +64,50 @@ export async function serveStatic(
     return false;
   }
 
-  if (meta.etag && !event[_kRaw].getResponseHeader("etag")) {
-    event[_kRaw].setResponseHeader("etag", meta.etag);
+  if (meta.etag && !event.response.headers.has("etag")) {
+    event.response.headers.set("etag", meta.etag);
   }
 
   const ifNotMatch =
-    meta.etag && event[_kRaw].getHeader("if-none-match") === meta.etag;
+    meta.etag && event.request.headers.get("if-none-match") === meta.etag;
   if (ifNotMatch) {
-    event[_kRaw].responseCode = 304;
-    event[_kRaw].responseMessage = "Not Modified";
+    event.response.status = 304;
+    event.response.statusText = "Not Modified";
     return "";
   }
 
   if (meta.mtime) {
     const mtimeDate = new Date(meta.mtime);
 
-    const ifModifiedSinceH = event[_kRaw].getHeader("if-modified-since");
+    const ifModifiedSinceH = event.request.headers.get("if-modified-since");
     if (ifModifiedSinceH && new Date(ifModifiedSinceH) >= mtimeDate) {
-      event[_kRaw].responseCode = 304;
-      event[_kRaw].responseMessage = "Not Modified";
+      event.response.status = 304;
+      event.response.statusText = "Not Modified";
       return "";
     }
 
-    if (!event[_kRaw].getResponseHeader("last-modified")) {
-      event[_kRaw].setResponseHeader("last-modified", mtimeDate.toUTCString());
+    if (!event.response.headers.get("last-modified")) {
+      event.response.headers.set("last-modified", mtimeDate.toUTCString());
     }
   }
 
-  if (meta.type && !event[_kRaw].getResponseHeader("content-type")) {
-    event[_kRaw].setResponseHeader("content-type", meta.type);
+  if (meta.type && !event.response.headers.get("content-type")) {
+    event.response.headers.set("content-type", meta.type);
   }
 
-  if (meta.encoding && !event[_kRaw].getResponseHeader("content-encoding")) {
-    event[_kRaw].setResponseHeader("content-encoding", meta.encoding);
+  if (meta.encoding && !event.response.headers.get("content-encoding")) {
+    event.response.headers.set("content-encoding", meta.encoding);
   }
 
   if (
     meta.size !== undefined &&
     meta.size > 0 &&
-    !event[_kRaw].getHeader("content-length")
+    !event.request.headers.get("content-length")
   ) {
-    event[_kRaw].setResponseHeader("content-length", meta.size + "");
+    event.response.headers.set("content-length", meta.size + "");
   }
 
-  if (event.method === "HEAD") {
+  if (event.request.method === "HEAD") {
     return "";
   }
 
diff --git a/tsconfig.json b/tsconfig.json
index 2338ce9..5d65890 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -10,5 +10,6 @@
     "noEmit": true,
     "types": ["node"]
   },
-  "include": ["src", "test"]
+  "include": ["src", "test"],
+  "exclude": ["h3-v1"]
 }


Test patch:
diff --git a/test/bench/bundle.test.ts b/test/bench/bundle.test.ts
index e6fe1a2..2a29021 100644
--- a/test/bench/bundle.test.ts
+++ b/test/bench/bundle.test.ts
@@ -6,13 +6,14 @@ import zlib from "node:zlib";
 describe("benchmark", () => {
   it("bundle size", async () => {
     const code = /* js */ `
-      import { createApp } from "../../dist/index.mjs";
-      export default createApp();
+      import { createH3 } from "../../dist/index.mjs";
+      // import { createH3 } from "../../src";
+      export default createH3();
     `;
     const { bytes, gzipSize } = await getBundleSize(code);
-    // console.log("bundle size", { bytes, gzipSize });
-    expect(bytes).toBeLessThanOrEqual(12_000); // <12kb
-    expect(gzipSize).toBeLessThanOrEqual(4500); // <4.5kb
+    // console.log({ bytes, gzipSize });
+    expect(bytes).toBeLessThanOrEqual(10_000); // <10kb
+    expect(gzipSize).toBeLessThanOrEqual(4000); // <4kb
   });
 });
 
diff --git a/test/bench/impl.ts b/test/bench/impl.ts
index 387ca48..913f455 100644
--- a/test/bench/impl.ts
+++ b/test/bench/impl.ts
@@ -4,7 +4,7 @@ import * as _h3v1 from "h3-v1";
 export function createInstances() {
   return [
     ["h3", h3(_h3src)],
-    ["h3-v1", h3v1()],
+    // ["h3-v1", h3v1()],
     ["maximum", fastest()],
   ] as const;
 }
@@ -17,13 +17,12 @@ export function h3(lib: typeof _h3src) {
 
   // [GET] /id/:id
   app.get("/id/:id", (event) => {
-    const query = lib.getQuery(event);
-    lib.setResponseHeader(event, "x-powered-by", "benchmark");
-    return `${event.context.params!.id} ${query.name}`;
+    event.response.headers.set("x-powered-by", "benchmark");
+    return `${event.context.params!.id} ${event.url.searchParams.get("name")}`;
   });
 
   // [POST] /json
-  app.post("/json", (event) => lib.readJSONBody(event));
+  app.post("/json", (event) => event.request.json());
 
   return app.fetch;
 }
@@ -59,22 +58,30 @@ export function h3v1() {
 }
 
 export function fastest() {
-  return function (req: Request): Response {
+  return function (req: Request): Response | Promise<Response> {
     const url = new URL(req.url);
-    if (req.method === "GET" && url.pathname === "/") {
-      return new Response("Hi");
-    }
-    if (req.method === "GET" && url.pathname.startsWith("/id/")) {
-      const id = url.pathname.slice(4);
-      const name = url.searchParams.get("name");
-      return new Response(`${id} ${name}`, {
-        headers: {
-          "x-powered-by": "benchmark",
-        },
-      });
-    }
-    if (req.method === "POST" && url.pathname === "/json") {
-      return new Response(`{"hello":"world"}`);
+    switch (req.method) {
+      case "GET": {
+        if (url.pathname === "/") {
+          return new Response("Hi");
+        }
+        if (url.pathname.startsWith("/id/")) {
+          const id = url.pathname.slice(4);
+          const name = url.searchParams.get("name");
+          return new Response(`${id} ${name}`, {
+            headers: {
+              "x-powered-by": "benchmark",
+            },
+          });
+        }
+        break;
+      }
+      case "POST": {
+        if (url.pathname === "/json") {
+          return req.json().then((body) => new Response(JSON.stringify(body)));
+        }
+        break;
+      }
     }
     return new Response("Not Found", { status: 404 });
   };
diff --git a/test/bench/index.ts b/test/bench/index.ts
index 7709339..7ad6038 100644
--- a/test/bench/index.ts
+++ b/test/bench/index.ts
@@ -2,6 +2,8 @@ import { bench, group as describe, run } from "mitata";
 import { requests } from "./input";
 import { createInstances } from "./impl";
 
+const runAll = process.argv.includes("--all");
+
 const instances = createInstances();
 
 describe("all", async () => {
@@ -21,19 +23,21 @@ describe("all", async () => {
   }
 });
 
-// for (const request of requests) {
-//   describe(`[${request.method}] ${request.path}`, () => {
-//     for (const [name, _fetch] of instances) {
-//       bench(name, async () => {
-//         await _fetch(
-//           new Request(`http://localhost${request.path}`, {
-//             method: request.method,
-//             body: request.body,
-//           }),
-//         );
-//       });
-//     }
-//   });
-// }
+if (runAll) {
+  for (const request of requests) {
+    describe(`[${request.method}] ${request.path}`, () => {
+      for (const [name, _fetch] of instances) {
+        bench(name, async () => {
+          await _fetch(
+            new Request(`http://localhost${request.path}`, {
+              method: request.method,
+              body: request.body,
+            }),
+          );
+        });
+      }
+    });
+  }
+}
 
 await run();
diff --git a/test/body.test.ts b/test/body.test.ts
index cd24982..9656a9a 100644
--- a/test/body.test.ts
+++ b/test/body.test.ts
@@ -1,15 +1,15 @@
 import { createReadStream } from "node:fs";
 import { readFile } from "node:fs/promises";
 import { describe, it, expect } from "vitest";
-import { readTextBody, readJSONBody, readFormDataBody } from "../src";
+import { readBody } from "../src";
 import { setupTest } from "./_setup";
 
 describe("body", () => {
   const ctx = setupTest({ startServer: true });
 
   it("can read simple string", async () => {
-    ctx.app.use("/api/test", async (request) => {
-      const body = await readTextBody(request);
+    ctx.app.use("/api/test", async (event) => {
+      const body = await event.request.text();
       expect(body).toEqual('{"bool":true,"name":"string","number":1}');
       return "200";
     });
@@ -28,8 +28,8 @@ describe("body", () => {
 
   it("can read chunked string", async () => {
     const requestJsonUrl = new URL("assets/sample.json", import.meta.url);
-    ctx.app.use("/api/test", async (request) => {
-      const body = await readTextBody(request);
+    ctx.app.use("/api/test", async (event) => {
+      const body = await event.request.text();
       const json = (await readFile(requestJsonUrl)).toString("utf8");
 
       expect(body).toEqual(json);
@@ -44,10 +44,10 @@ describe("body", () => {
     expect(await result.body.text()).toBe("200");
   });
 
-  it("returns undefined if body is not present", async () => {
+  it("returns empty string if body is not present", async () => {
     let _body: string | undefined = "initial";
-    ctx.app.use("/api/test", async (request) => {
-      _body = await readTextBody(request);
+    ctx.app.use("/api/test", async (event) => {
+      _body = await event.request.text();
       return "200";
     });
     const result = await ctx.client!.request({
@@ -55,14 +55,14 @@ describe("body", () => {
       method: "POST",
     });
 
-    expect(_body).toBeUndefined();
+    expect(_body).toBe("");
     expect(await result.body.text()).toBe("200");
   });
 
   it("returns an empty string if body is string", async () => {
     let _body: string | undefined = "initial";
-    ctx.app.use("/api/test", async (request) => {
-      _body = await readJSONBody(request);
+    ctx.app.use("/api/test", async (event) => {
+      _body = await readBody(event);
       return "200";
     });
     const result = await ctx.client!.request({
@@ -77,8 +77,8 @@ describe("body", () => {
 
   it("returns an empty object string if body is empty object", async () => {
     let _body: string | undefined = "initial";
-    ctx.app.use("/api/test", async (request) => {
-      _body = await readTextBody(request);
+    ctx.app.use("/api/test", async (event) => {
+      _body = await readBody(event);
       return "200";
     });
     const result = await ctx.client!.request({
@@ -87,13 +87,14 @@ describe("body", () => {
       body: "{}",
     });
 
-    expect(_body).toBe("{}");
+    expect(_body).toMatchObject({});
+    expect(Object.keys(_body).length).toBe(0);
     expect(await result.body.text()).toBe("200");
   });
 
   it("can parse json payload", async () => {
-    ctx.app.use("/api/test", async (request) => {
-      const body = await readJSONBody(request);
+    ctx.app.use("/api/test", async (event) => {
+      const body = await readBody(event);
       expect(body).toMatchObject({
         bool: true,
         name: "string",
@@ -116,8 +117,8 @@ describe("body", () => {
 
   it("handles non-present body", async () => {
     let _body: string | undefined;
-    ctx.app.use("/api/test", async (request) => {
-      _body = await readJSONBody(request);
+    ctx.app.use("/api/test", async (event) => {
+      _body = await readBody(event);
       return "200";
     });
     const result = await ctx.client!.request({
@@ -128,10 +129,10 @@ describe("body", () => {
     expect(await result.body.text()).toBe("200");
   });
 
-  it("handles empty body", async () => {
+  it("handles empty string body", async () => {
     let _body: string | undefined = "initial";
-    ctx.app.use("/api/test", async (request) => {
-      _body = await readTextBody(request);
+    ctx.app.use("/api/test", async (event) => {
+      _body = await readBody(event);
       return "200";
     });
     const result = await ctx.client!.request({
@@ -142,14 +143,14 @@ describe("body", () => {
       },
       body: '""',
     });
-    expect(_body).toStrictEqual('""');
+    expect(_body).toStrictEqual("");
     expect(await result.body.text()).toBe("200");
   });
 
   it("handles empty object as body", async () => {
     let _body: string | undefined = "initial";
-    ctx.app.use("/api/test", async (request) => {
-      _body = await readJSONBody(request);
+    ctx.app.use("/api/test", async (event) => {
+      _body = await readBody(event);
       return "200";
     });
     const result = await ctx.client!.request({
@@ -157,13 +158,14 @@ describe("body", () => {
       method: "POST",
       body: "{}",
     });
-    expect(_body).toStrictEqual({});
+    expect(_body).toMatchObject({});
+    expect(Object.keys(_body).length).toBe(0);
     expect(await result.body.text()).toBe("200");
   });
 
   it("parse the form encoded into an object", async () => {
-    ctx.app.use("/api/test", async (request) => {
-      const body = await readJSONBody(request);
+    ctx.app.use("/api/test", async (event) => {
+      const body = await readBody(event);
       expect(body).toMatchObject({
         field: "value",
         another: "true",
@@ -184,9 +186,9 @@ describe("body", () => {
   });
 
   it("parses multipart form data", async () => {
-    ctx.app.use("/api/test", async (request) => {
-      const formData = await readFormDataBody(request);
-      return [...formData!.entries()].map(([name, value]) => ({
+    ctx.app.use("/api/test", async (event) => {
+      const formData = await event.request.formData();
+      return [...formData.entries()].map(([name, value]) => ({
         name,
         data: value,
       }));
@@ -220,10 +222,10 @@ describe("body", () => {
       `);
   });
 
-  it("returns undefined if body is not present with text/plain", async () => {
+  it("returns empty string if body is not present with text/plain", async () => {
     let _body: string | undefined;
-    ctx.app.use("/api/test", async (request) => {
-      _body = await readTextBody(request);
+    ctx.app.use("/api/test", async (event) => {
+      _body = await event.request.text();
       return "200";
     });
     const result = await ctx.client!.request({
@@ -234,14 +236,14 @@ describe("body", () => {
       },
     });
 
-    expect(_body).toBeUndefined();
+    expect(_body).toBe("");
     expect(await result.body.text()).toBe("200");
   });
 
   it("returns undefined if body is not present with json", async () => {
     let _body: string | undefined;
-    ctx.app.use("/api/test", async (request) => {
-      _body = await readTextBody(request);
+    ctx.app.use("/api/test", async (event) => {
+      _body = await readBody(event);
       return "200";
     });
     const result = await ctx.client!.request({
@@ -258,8 +260,8 @@ describe("body", () => {
 
   it("returns the string if content type is text/*", async () => {
     let _body: string | undefined;
-    ctx.app.use("/api/test", async (request) => {
-      _body = await readTextBody(request);
+    ctx.app.use("/api/test", async (event) => {
+      _body = await event.request.text();
       return "200";
     });
     const result = await ctx.client!.request({
@@ -276,8 +278,8 @@ describe("body", () => {
   });
 
   it("returns string as is if cannot parse with unknown content type", async () => {
-    ctx.app.use("/api/test", async (request) => {
-      const _body = await readTextBody(request);
+    ctx.app.use("/api/test", async (event) => {
+      const _body = await event.request.text();
       return _body;
     });
     const result = await ctx.client!.request({
@@ -294,8 +296,8 @@ describe("body", () => {
   });
 
   it("fails if json is invalid", async () => {
-    ctx.app.use("/api/test", async (request) => {
-      const _body = await readJSONBody(request);
+    ctx.app.use("/api/test", async (event) => {
+      const _body = await readBody(event);
       return _body;
     });
     const result = await ctx.client!.request({
diff --git a/test/event.test.ts b/test/event.test.ts
index d92a61e..5cd187e 100644
--- a/test/event.test.ts
+++ b/test/event.test.ts
@@ -1,5 +1,5 @@
 import { describe, it, expect } from "vitest";
-import { readJSONBody, getBodyStream, getRequestURL } from "../src";
+import { readBody, getRequestURL } from "../src";
 import { setupTest } from "./_setup";
 
 describe("Event", () => {
@@ -7,8 +7,8 @@ describe("Event", () => {
 
   it("can read the method", async () => {
     ctx.app.use("/*", (event) => {
-      expect(event.method).toBe(event.method);
-      expect(event.method).toBe("POST");
+      expect(event.request.method).toBe(event.request.method);
+      expect(event.request.method).toBe("POST");
       return "200";
     });
     const result = await ctx.request.post("/hello");
@@ -18,7 +18,7 @@ describe("Event", () => {
   it("can read the headers", async () => {
     ctx.app.use("/*", (event) => {
       return {
-        headers: [...event.headers.entries()],
+        headers: [...event.request.headers.entries()],
       };
     });
     const result = await ctx.request
@@ -42,10 +42,9 @@ describe("Event", () => {
 
   it("can read request body", async () => {
     ctx.app.use("/*", async (event) => {
-      const bodyStream = getBodyStream(event);
       let bytes = 0;
       // @ts-expect-error iterator
-      for await (const chunk of bodyStream!) {
+      for await (const chunk of event.request.body!) {
         bytes += chunk.length;
       }
       return {
@@ -62,9 +61,9 @@ describe("Event", () => {
 
   it("can convert to a web request", async () => {
     ctx.app.use("/", async (event) => {
-      expect(event.method).toBe("POST");
-      expect(event.headers.get("x-test")).toBe("123");
-      expect(await readJSONBody(event)).toMatchObject({ hello: "world" });
+      expect(event.request.method).toBe("POST");
+      expect(event.request.headers.get("x-test")).toBe("123");
+      expect(await readBody(event)).toMatchObject({ hello: "world" });
       return "200";
     });
     const result = await ctx.request
diff --git a/test/fixture/app.ts b/test/fixture/app.ts
index c8f6596..315c28b 100644
--- a/test/fixture/app.ts
+++ b/test/fixture/app.ts
@@ -1,22 +1,14 @@
-import {
-  createApp,
-  createRouter,
-  getQuery,
-  getRequestHeaders,
-} from "../../src";
+import { createH3, getQuery } from "../../src";
 
-export const app = createApp();
+export const app = createH3();
 
-const router = createRouter();
-app.use(router);
-
-router.get("/**", (event) => {
+app.get("/**", (event) => {
   return {
     request: {
-      method: event.method,
+      method: event.request.method,
       path: event.path,
       params: getQuery(event),
-      headers: getRequestHeaders(event),
+      headers: Object.fromEntries(event.request.headers.entries()),
     },
   };
 });
diff --git a/test/header.test.ts b/test/header.test.ts
deleted file mode 100644
index b790e70..0000000
--- a/test/header.test.ts
+++ /dev/null
@@ -1,247 +0,0 @@
-import { describe, it, expect } from "vitest";
-import {
-  getRequestHeaders,
-  getRequestHeader,
-  setResponseHeaders,
-  setResponseHeader,
-  appendResponseHeaders,
-  appendResponseHeader,
-  removeResponseHeader,
-  clearResponseHeaders,
-} from "../src";
-import { setupTest } from "./_setup";
-
-describe("", () => {
-  const ctx = setupTest();
-
-  describe("getRequestHeaders", () => {
-    it("can return request headers", async () => {
-      ctx.app.use("/", (event) => {
-        const headers = getRequestHeaders(event);
-        expect(headers).toMatchObject({
-          accept: "application/json",
-        });
-      });
-      await ctx.request.get("/").set("Accept", "application/json");
-    });
-  });
-
-  describe("getRequestHeaders", () => {
-    it("can return request headers", async () => {
-      ctx.app.use("/", (event) => {
-        const headers = getRequestHeaders(event);
-        expect(headers).toMatchObject({ accept: "application/json" });
-      });
-      await ctx.request.get("/").set("Accept", "application/json");
-    });
-  });
-
-  describe("getRequestHeader", () => {
-    it("can return a value of request header corresponding to the given name", async () => {
-      ctx.app.use("/", (event) => {
-        expect(getRequestHeader(event, "accept")).toEqual("application/json");
-        expect(getRequestHeader(event, "Accept")).toEqual("application/json");
-        expect(getRequestHeader(event, "cookie")).toEqual("a; b; c");
-      });
-      await ctx.request
-        .get("/")
-        .set("Accept", "application/json")
-        .set("Cookie", ["a", "b", "c"]);
-    });
-  });
-
-  describe("getRequestHeader", () => {
-    it("can return a value of request header corresponding to the given name", async () => {
-      ctx.app.use("/", (event) => {
-        expect(getRequestHeader(event, "accept")).toEqual("application/json");
-        expect(getRequestHeader(event, "Accept")).toEqual("application/json");
-      });
-      await ctx.request.get("/").set("Accept", "application/json");
-    });
-  });
-
-  describe("setResponseHeaders", () => {
-    it("can set multiple values to multiple response headers corresponding to the given object", async () => {
-      ctx.app.use("/", (event) => {
-        setResponseHeaders(event, {
-          "X-HTTP-Header-1": "string-value-1",
-          "X-HTTP-Header-2": "string-value-2",
-        });
-      });
-      const result = await ctx.request.get("/");
-      expect(result.headers["x-http-header-1"]).toEqual("string-value-1");
-      expect(result.headers["x-http-header-2"]).toEqual("string-value-2");
-    });
-  });
-
-  describe("setResponseHeaders", () => {
-    it("can set multiple values to multiple response headers corresponding to the given object", async () => {
-      ctx.app.use("/", (event) => {
-        setResponseHeaders(event, {
-          "X-HTTP-Header-1": "string-value-1",
-          "X-HTTP-Header-2": "string-value-2",
-        });
-      });
-      const result = await ctx.request.get("/");
-      expect(result.headers["x-http-header-1"]).toEqual("string-value-1");
-      expect(result.headers["x-http-header-2"]).toEqual("string-value-2");
-    });
-  });
-
-  describe("setResponseHeader", () => {
-    it("can set a string value to response header corresponding to the given name", async () => {
-      ctx.app.use("/", (event) => {
-        setResponseHeader(event, "X-HTTP-Header", "string-value");
-      });
-      const result = await ctx.request.get("/");
-      expect(result.headers["x-http-header"]).toEqual("string-value");
-    });
-
-    it("can set a number value to response header corresponding to the given name", async () => {
-      ctx.app.use("/", (event) => {
-        setResponseHeader(event, "X-HTTP-Header", 12_345 as unknown as string);
-      });
-      const result = await ctx.request.get("/");
-      expect(result.headers["x-http-header"]).toEqual("12345");
-    });
-
-    it("can set an array value to response header corresponding to the given name", async () => {
-      ctx.app.use("/", (event) => {
-        setResponseHeader(event, "X-HTTP-Header", ["value 1", "value 2"]);
-        setResponseHeader(event, "X-HTTP-Header", ["value 3", "value 4"]);
-      });
-      const result = await ctx.request.get("/");
-      expect(result.headers["x-http-header"]).toEqual("value 3, value 4");
-    });
-  });
-
-  describe("setResponseHeader", () => {
-    it("can set a string value to response header corresponding to the given name", async () => {
-      ctx.app.use("/", (event) => {
-        setResponseHeader(event, "X-HTTP-Header", "string-value");
-      });
-      const result = await ctx.request.get("/");
-      expect(result.headers["x-http-header"]).toEqual("string-value");
-    });
-
-    it("can set a number value to response header corresponding to the given name", async () => {
-      ctx.app.use("/", (event) => {
-        setResponseHeader(event, "X-HTTP-Header", "12345");
-      });
-      const result = await ctx.request.get("/");
-      expect(result.headers["x-http-header"]).toEqual("12345");
-    });
-
-    it("can set an array value to response header corresponding to the given name", async () => {
-      ctx.app.use("/", (event) => {
-        setResponseHeader(event, "X-HTTP-Header", ["value 1", "value 2"]);
-        setResponseHeader(event, "X-HTTP-Header", ["value 3", "value 4"]);
-      });
-      const result = await ctx.request.get("/");
-      expect(result.headers["x-http-header"]).toEqual("value 3, value 4");
-    });
-  });
-
-  describe("appendResponseHeaders", () => {
-    it("can append multiple string values to multiple response header corresponding to the given object", async () => {
-      ctx.app.use("/", (event) => {
-        appendResponseHeaders(event, {
-          "X-HTTP-Header-1": "string-value-1-1",
-          "X-HTTP-Header-2": "string-value-2-1",
-        });
-        appendResponseHeaders(event, {
-          "X-HTTP-Header-1": "string-value-1-2",
-          "X-HTTP-Header-2": "string-value-2-2",
-        });
-      });
-      const result = await ctx.request.get("/");
-      expect(result.headers["x-http-header-1"]).toEqual(
-        "string-value-1-1, string-value-1-2",
-      );
-      expect(result.headers["x-http-header-2"]).toEqual(
-        "string-value-2-1, string-value-2-2",
-      );
-    });
-  });
-
-  describe("appendResponseHeaders", () => {
-    it("can append multiple string values to multiple response header corresponding to the given object", async () => {
-      ctx.app.use("/", (event) => {
-        appendResponseHeaders(event, {
-          "X-HTTP-Header-1": "string-value-1-1",
-          "X-HTTP-Header-2": "string-value-2-1",
-        });
-        appendResponseHeaders(event, {
-          "X-HTTP-Header-1": "string-value-1-2",
-          "X-HTTP-Header-2": "string-value-2-2",
-        });
-      });
-      const result = await ctx.request.get("/");
-      expect(result.headers["x-http-header-1"]).toEqual(
-        "string-value-1-1, string-value-1-2",
-      );
-      expect(result.headers["x-http-header-2"]).toEqual(
-        "string-value-2-1, string-value-2-2",
-      );
-    });
-  });
-
-  describe("appendResponseHeader", () => {
-    it("can append a value to response header corresponding to the given name", async () => {
-      ctx.app.use("/", (event) => {
-        appendResponseHeader(event, "X-HTTP-Header", "value 1");
-        appendResponseHeader(event, "X-HTTP-Header", "value 2");
-      });
-      const result = await ctx.request.get("/");
-      expect(result.headers["x-http-header"]).toEqual("value 1, value 2");
-    });
-  });
-
-  describe("appendHeader", () => {
-    it("can append a value to response header corresponding to the given name", async () => {
-      ctx.app.use("/", (event) => {
-        appendResponseHeader(event, "X-HTTP-Header", "value 1");
-        appendResponseHeader(event, "X-HTTP-Header", "value 2");
-      });
-      const result = await ctx.request.get("/");
-      expect(result.headers["x-http-header"]).toEqual("value 1, value 2");
-    });
-  });
-
-  describe("clearResponseHeaders", () => {
-    it("can remove all response headers", async () => {
-      ctx.app.use("/", (event) => {
-        appendResponseHeader(event, "header-1", "1");
-        appendResponseHeader(event, "header-2", "2");
-        clearResponseHeaders(event);
-      });
-      const result = await ctx.request.get("/");
-      expect(result.headers["header-1"]).toBeUndefined();
-      expect(result.headers["header-2"]).toBeUndefined();
-    });
-
-    it("can remove multiple response headers", async () => {
-      ctx.app.use("/", (event) => {
-        appendResponseHeader(event, "header-3", "3");
-        appendResponseHeader(event, "header-4", "4");
-        appendResponseHeader(event, "header-5", "5");
-        clearResponseHeaders(event, ["header-3", "header-5"]);
-      });
-      const result = await ctx.request.get("/");
-      expect(result.headers["header-3"]).toBeUndefined();
-      expect(result.headers["header-4"]).toBe("4");
-      expect(result.headers["header-5"]).toBeUndefined();
-    });
-
-    it("can remove a single response header", async () => {
-      ctx.app.use("/", (event) => {
-        appendResponseHeader(event, "header-6", "6");
-        appendResponseHeader(event, "header-7", "7");
-        removeResponseHeader(event, "header-6");
-      });
-      const result = await ctx.request.get("/");
-      expect(result.headers["header-6"]).toBeUndefined();
-      expect(result.headers["header-7"]).toBe("7");
-    });
-  });
-});
diff --git a/test/iterable.test.ts b/test/iterable.test.ts
index 1928a85..666f997 100644
--- a/test/iterable.test.ts
+++ b/test/iterable.test.ts
@@ -34,14 +34,14 @@ describe("iterable", () => {
 
   describe("iterable", () => {
     it("sends empty body for an empty iterator", async () => {
-      ctx.app.use(() => iterable([]));
+      ctx.app.use((event) => iterable(event, []));
       const result = await ctx.request.get("/");
       expect(result.header["content-length"]).toBe("0");
       expect(result.text).toBe("");
     });
 
     it("concatenates iterated values", async () => {
-      ctx.app.use(() => iterable(["a", "b", "c"]));
+      ctx.app.use((event) => iterable(event, ["a", "b", "c"]));
       const result = await ctx.request.get("/");
       expect(result.text).toBe("abc");
     });
@@ -114,7 +114,7 @@ describe("iterable", () => {
           }),
         },
       ])("$type", async (t) => {
-        ctx.app.use(() => iterable(t.iterable));
+        ctx.app.use((event) => iterable(event, t.iterable));
         const response = await ctx.request.get("/");
         expect(response.text).toBe("the-value");
       });
@@ -124,7 +124,7 @@ describe("iterable", () => {
       it("is called for every value", async () => {
         const testIterable = [1, "2", { field: 3 }, null];
         const serializer = vi.fn(() => "x");
-        ctx.app.use(() => iterable(testIterable, { serializer }));
+        ctx.app.use((event) => iterable(event, testIterable, { serializer }));
         const response = await ctx.request.get("/");
         expect(response.text).toBe("x".repeat(testIterable.length));
         expect(serializer).toBeCalledTimes(4);
diff --git a/test/package.test.ts b/test/package.test.ts
index 7636fe0..daf6556 100644
--- a/test/package.test.ts
+++ b/test/package.test.ts
@@ -41,7 +41,6 @@ describe("h3 package", () => {
         "getHeader",
         "getHeaders",
         "getMethod",
-        "getNodeContext",
         "getProxyRequestHeaders",
         "getQuery",
         "getRequestFingerprint",
@@ -62,7 +61,6 @@ describe("h3 package", () => {
         "getSession",
         "getValidatedQuery",
         "getValidatedRouterParams",
-        "getWebContext",
         "handleCacheHeaders",
         "handleCors",
         "isCorsOriginAllowed",
@@ -80,11 +78,8 @@ describe("h3 package", () => {
         "readBody",
         "readFormData",
         "readFormDataBody",
-        "readJSONBody",
         "readRawBody",
-        "readTextBody",
         "readValidatedBody",
-        "readValidatedJSONBody",
         "redirect",
         "removeResponseHeader",
         "sanitizeStatusCode",
@@ -108,7 +103,6 @@ describe("h3 package", () => {
         "toNodeHandler",
         "toNodeListener",
         "toWebHandler",
-        "toWebRequest",
         "unsealSession",
         "updateSession",
         "useBase",
diff --git a/test/proxy.test.ts b/test/proxy.test.ts
index b798290..b11102d 100644
--- a/test/proxy.test.ts
+++ b/test/proxy.test.ts
@@ -1,13 +1,6 @@
 import { readFile } from "node:fs/promises";
 import { describe, it, expect, beforeEach, vi } from "vitest";
-import {
-  getRequestHeaders,
-  setCookie,
-  setResponseHeader,
-  readTextBody,
-  readRawBody,
-  appendResponseHeader,
-} from "../src";
+import { setCookie } from "../src";
 import { proxy, proxyRequest } from "../src/utils/proxy";
 import { setupTest } from "./_setup";
 
@@ -32,10 +25,10 @@ describe("proxy", () => {
   describe("proxyRequest", () => {
     it("can proxy request", async () => {
       ctx.app.use("/debug", async (event) => {
-        const headers = getRequestHeaders(event);
-        const body = await readTextBody(event);
+        const headers = Object.fromEntries(event.request.headers.entries());
+        const body = await event.request.text();
         return {
-          method: event.method,
+          method: event.request.method,
           headers,
           body,
         };
@@ -79,15 +72,15 @@ describe("proxy", () => {
 
     it("can proxy binary request", async () => {
       ctx.app.use("/debug", async (event) => {
-        const body = await readRawBody(event);
+        const body = await event.request.arrayBuffer();
         return {
-          headers: getRequestHeaders(event),
-          bytes: body!.length,
+          headers: Object.fromEntries(event.request.headers.entries()),
+          bytes: body.byteLength,
         };
       });
 
       ctx.app.use("/", (event) => {
-        setResponseHeader(event, "x-res-header", "works");
+        event.response.headers.set("x-res-header", "works");
         return proxyRequest(event, ctx.url + "/debug", { fetch });
       });
 
@@ -112,8 +105,8 @@ describe("proxy", () => {
     it("can proxy stream request", async () => {
       ctx.app.use("/debug", async (event) => {
         return {
-          body: await readTextBody(event),
-          headers: getRequestHeaders(event),
+          body: await event.request.text(),
+          headers: Object.fromEntries(event.request.headers.entries()),
         };
       });
 
@@ -161,7 +154,7 @@ describe("proxy", () => {
       const message = '{"hello":"world"}';
 
       ctx.app.use("/debug", (event) => {
-        setResponseHeader(event, "content-type", "application/json");
+        event.response.headers.set("content-type", "application/json");
         return message;
       });
 
@@ -226,8 +219,7 @@ describe("proxy", () => {
   describe("cookieDomainRewrite", () => {
     beforeEach(() => {
       ctx.app.use("/debug", (event) => {
-        setResponseHeader(
-          event,
+        event.response.headers.set(
           "set-cookie",
           "foo=219ffwef9w0f; Domain=somecompany.co.uk; Path=/; Expires=Wed, 30 Aug 2022 00:00:00 GMT",
         );
@@ -269,13 +261,11 @@ describe("proxy", () => {
 
     it("can rewrite domains of multiple cookies", async () => {
       ctx.app.use("/multiple/debug", (event) => {
-        appendResponseHeader(
-          event,
+        event.response.headers.append(
           "set-cookie",
           "foo=219ffwef9w0f; Domain=somecompany.co.uk; Path=/; Expires=Wed, 30 Aug 2022 00:00:00 GMT",
         );
-        appendResponseHeader(
-          event,
+        event.response.headers.append(
           "set-cookie",
           "bar=38afes7a8; Domain=somecompany.co.uk; Path=/; Expires=Wed, 30 Aug 2022 00:00:00 GMT",
         );
@@ -319,8 +309,7 @@ describe("proxy", () => {
   describe("cookiePathRewrite", () => {
     beforeEach(() => {
       ctx.app.use("/debug", (event) => {
-        setResponseHeader(
-          event,
+        event.response.headers.set(
           "set-cookie",
           "foo=219ffwef9w0f; Domain=somecompany.co.uk; Path=/; Expires=Wed, 30 Aug 2022 00:00:00 GMT",
         );
@@ -362,10 +351,14 @@ describe("proxy", () => {
 
     it("can rewrite paths of multiple cookies", async () => {
       ctx.app.use("/multiple/debug", (event) => {
-        appendResponseHeader(event, "set-cookie", [
+        event.response.headers.append(
+          "set-cookie",
           "foo=219ffwef9w0f; Domain=somecompany.co.uk; Path=/; Expires=Wed, 30 Aug 2022 00:00:00 GMT",
+        );
+        event.response.headers.append(
+          "set-cookie",
           "bar=38afes7a8; Domain=somecompany.co.uk; Path=/; Expires=Wed, 30 Aug 2022 00:00:00 GMT",
-        ]);
+        );
         return {};
       });
 
@@ -417,8 +410,8 @@ describe("proxy", () => {
       ctx.app.use("/", (event) => {
         return proxyRequest(event, ctx.url + "/debug", {
           fetch,
-          onResponse(_event) {
-            setResponseHeader(_event, "x-custom", "hello");
+          onResponse(event) {
+            event.response.headers.set("x-custom", "hello");
           },
         });
       });
@@ -434,7 +427,7 @@ describe("proxy", () => {
           fetch,
           onResponse(_event) {
             return new Promise((resolve) => {
-              resolve(setResponseHeader(_event, "x-custom", "hello"));
+              resolve(event.response.headers.set("x-custom", "hello"));
             });
           },
         });
diff --git a/test/router.test.ts b/test/router.test.ts
index 81343fa..47a2089 100644
--- a/test/router.test.ts
+++ b/test/router.test.ts
@@ -1,6 +1,6 @@
 import type { H3 } from "../src/types";
 import { describe, it, expect, beforeEach } from "vitest";
-import { getRouterParams, getRouterParam, createApp } from "../src";
+import { getRouterParams, getRouterParam, createH3 } from "../src";
 import { setupTest } from "./_setup";
 
 describe("router", () => {
@@ -9,7 +9,7 @@ describe("router", () => {
   let router: H3;
 
   beforeEach(() => {
-    router = createApp()
+    router = createH3()
       .get("/", () => "Hello")
       .get("/test/?/a", () => "/test/?/a")
       .get("/many/routes", () => "many routes")
@@ -26,7 +26,7 @@ describe("router", () => {
   });
 
   it("Multiple Routers", async () => {
-    const secondRouter = createApp().get("/router2", () => "router2");
+    const secondRouter = createH3().get("/router2", () => "router2");
 
     ctx.app.use(secondRouter);
 
@@ -71,9 +71,15 @@ describe("router", () => {
   });
 
   it("Handle shadowed route", async () => {
-    router.post("/test/123", (event) => `[${event.method}] ${event.path}`);
+    router.post(
+      "/test/123",
+      (event) => `[${event.request.method}] ${event.path}`,
+    );
 
-    router.use("/test/**", (event) => `[${event.method}] ${event.path}`);
+    router.use(
+      "/test/**",
+      (event) => `[${event.request.method}] ${event.path}`,
+    );
 
     // Loop to validate cached behavior
     for (let i = 0; i < 5; i++) {
@@ -94,7 +100,7 @@ describe("router (preemptive)", () => {
   let router: H3;
 
   beforeEach(() => {
-    router = createApp()
+    router = createH3()
       .get("/test", () => "Test")
       .get("/undefined", () => undefined);
     ctx.app.all("/**", router);
@@ -129,7 +135,7 @@ describe("getRouterParams", () => {
 
   describe("with router", () => {
     it("can return router params", async () => {
-      const router = createApp().get("/test/params/:name", (event) => {
+      const router = createH3().get("/test/params/:name", (event) => {
         expect(getRouterParams(event)).toMatchObject({ name: "string" });
         return "200";
       });
@@ -140,7 +146,7 @@ describe("getRouterParams", () => {
     });
 
     it("can decode router params", async () => {
-      const router = createApp().get("/test/params/:name", (event) => {
+      const router = createH3().get("/test/params/:name", (event) => {
         expect(getRouterParams(event, { decode: true })).toMatchObject({
           name: "string with space",
         });
@@ -171,7 +177,7 @@ describe("getRouterParam", () => {
 
   describe("with router", () => {
     it("can return a value of router params corresponding to the given name", async () => {
-      const router = createApp().get("/test/params/:name", (event) => {
+      const router = createH3().get("/test/params/:name", (event) => {
         expect(getRouterParam(event, "name")).toEqual("string");
         return "200";
       });
@@ -182,7 +188,7 @@ describe("getRouterParam", () => {
     });
 
     it("can decode a value of router params corresponding to the given name", async () => {
-      const router = createApp().get("/test/params/:name", (event) => {
+      const router = createH3().get("/test/params/:name", (event) => {
         expect(getRouterParam(event, "name", { decode: true })).toEqual(
           "string with space",
         );
@@ -213,7 +219,7 @@ describe("event.context.matchedRoute", () => {
 
   describe("with router", () => {
     it("can return the matched path", async () => {
-      const router = createApp().get("/test/:template", (event) => {
+      const router = createH3().get("/test/:template", (event) => {
         expect(event.context.matchedRoute).toMatchObject({
           method: "GET",
           route: "/test/:template",
diff --git a/test/session.test.ts b/test/session.test.ts
index 58868ef..80617a4 100644
--- a/test/session.test.ts
+++ b/test/session.test.ts
@@ -1,6 +1,6 @@
 import type { SessionConfig } from "../src/types";
 import { describe, it, expect, beforeEach } from "vitest";
-import { useSession, readJSONBody, createApp } from "../src";
+import { useSession, readBody, createApp } from "../src";
 import { setupTest } from "./_setup";
 
 describe("session", () => {
@@ -21,8 +21,8 @@ describe("session", () => {
     router = createApp({});
     router.use("/", async (event) => {
       const session = await useSession(event, sessionConfig);
-      if (event.method === "POST") {
-        await session.update((await readJSONBody(event)) as any);
+      if (event.request.method === "POST") {
+        await session.update((await readBody(event)) as any);
       }
       return { session };
     });
diff --git a/test/types.test-d.ts b/test/types.test-d.ts
index 3a84c9d..17c7a69 100644
--- a/test/types.test-d.ts
+++ b/test/types.test-d.ts
@@ -1,11 +1,10 @@
-import type { QueryObject } from "ufo";
 import type { H3Event } from "../src/types";
 import { describe, it, expectTypeOf } from "vitest";
 import {
   defineEventHandler,
   getQuery,
-  readJSONBody,
-  readValidatedJSONBody,
+  readBody,
+  readValidatedBody,
   getValidatedQuery,
 } from "../src";
 
@@ -21,7 +20,7 @@ describe("types", () => {
         async handler(event) {
           expectTypeOf(event).toEqualTypeOf<H3Event>();
 
-          const body = await readJSONBody(event);
+          const body = await readBody(event);
           expectTypeOf(body).toBeUnknown();
 
           return {
@@ -52,17 +51,17 @@ describe("types", () => {
     });
   });
 
-  describe("readJSONBody", () => {
+  describe("readBody", () => {
     it("untyped", () => {
       defineEventHandler(async (event) => {
-        const body = await readJSONBody(event);
+        const body = await readBody(event);
         expectTypeOf(body).toBeUnknown();
       });
     });
 
     it("typed via generic", () => {
       defineEventHandler(async (event) => {
-        const body = await readJSONBody<string>(event);
+        const body = await readBody<string>(event);
         expectTypeOf(body).not.toBeAny();
         expectTypeOf(body!).toBeString();
       });
@@ -71,7 +70,7 @@ describe("types", () => {
     it("typed via validator", () => {
       defineEventHandler(async (event) => {
         const validator = (body: unknown) => body as { id: string };
-        const body = await readValidatedJSONBody(event, validator);
+        const body = await readValidatedBody(event, validator);
         expectTypeOf(body).not.toBeAny();
         expectTypeOf(body).toEqualTypeOf<{ id: string }>();
       });
@@ -79,7 +78,7 @@ describe("types", () => {
 
     it("typed via event handler", () => {
       defineEventHandler<{ body: { id: string } }>(async (event) => {
-        const body = await readJSONBody(event);
+        const body = await readBody(event);
         expectTypeOf(body).not.toBeAny();
         expectTypeOf(body).toEqualTypeOf<{ id: string } | undefined>();
       });
@@ -91,7 +90,7 @@ describe("types", () => {
       defineEventHandler((event) => {
         const query = getQuery(event);
         expectTypeOf(query).not.toBeAny();
-        expectTypeOf(query).toEqualTypeOf<QueryObject>();
+        expectTypeOf(query).toEqualTypeOf<Record<string, string>>();
       });
     });
 
diff --git a/test/utils.test.ts b/test/utils.test.ts
index 61d108b..14ed80a 100644
--- a/test/utils.test.ts
+++ b/test/utils.test.ts
@@ -2,16 +2,14 @@ import { ReadableStream } from "node:stream/web";
 import { describe, it, expect, vi } from "vitest";
 import {
   redirect,
-  useBase,
+  withBase,
   assertMethod,
   getQuery,
   getRequestURL,
-  readFormDataBody,
   getRequestIP,
   getRequestFingerprint,
   iterable,
 } from "../src";
-import { getNodeContext } from "../src/adapters/node";
 import { serializeIterableValue } from "../src/utils/internal/iterable";
 import { setupTest } from "./_setup";
 
@@ -55,14 +53,14 @@ describe("", () => {
 
   describe("iterable", () => {
     it("sends empty body for an empty iterator", async () => {
-      ctx.app.use(() => iterable([]));
+      ctx.app.use((event) => iterable(event, []));
       const result = await ctx.request.get("/");
       expect(result.header["content-length"]).toBe("0");
       expect(result.text).toBe("");
     });
 
     it("concatenates iterated values", async () => {
-      ctx.app.use(() => iterable(["a", "b", "c"]));
+      ctx.app.use((event) => iterable(event, ["a", "b", "c"]));
       const result = await ctx.request.get("/");
       expect(result.text).toBe("abc");
     });
@@ -135,7 +133,7 @@ describe("", () => {
           }),
         },
       ])("$type", async (t) => {
-        ctx.app.use(() => iterable(t.iterable));
+        ctx.app.use((event) => iterable(event, t.iterable));
         const response = await ctx.request.get("/");
         expect(response.text).toBe("the-value");
       });
@@ -146,7 +144,7 @@ describe("", () => {
         const testIterable = [1, "2", { field: 3 }, null];
         const serializer = vi.fn(() => "x");
 
-        ctx.app.use(() => iterable(testIterable, { serializer }));
+        ctx.app.use((event) => iterable(event, testIterable, { serializer }));
         const response = await ctx.request.get("/");
         expect(response.text).toBe("x".repeat(testIterable.length));
         expect(serializer).toBeCalledTimes(4);
@@ -157,11 +155,11 @@ describe("", () => {
     });
   });
 
-  describe("useBase", () => {
+  describe("withBase", () => {
     it("can prefix routes", async () => {
       ctx.app.use(
         "/**",
-        useBase("/api", (event) => Promise.resolve(event.path)),
+        withBase("/api", (event) => Promise.resolve(event.path)),
       );
       const result = await ctx.request.get("/api/test");
 
@@ -170,7 +168,7 @@ describe("", () => {
     it("does nothing when not provided a base", async () => {
       ctx.app.use(
         "/**",
-        useBase("", (event) => Promise.resolve(event.path)),
+        withBase("", (event) => Promise.resolve(event.path)),
       );
       const result = await ctx.request.get("/api/test");
 
@@ -199,7 +197,7 @@ describe("", () => {
 
   describe("getMethod", () => {
     it("can get method", async () => {
-      ctx.app.use("/*", (event) => event.method);
+      ctx.app.use("/*", (event) => event.request.method);
       expect((await ctx.request.get("/api")).text).toBe("GET");
       expect((await ctx.request.post("/api")).text).toBe("POST");
     });
@@ -367,8 +365,7 @@ describe("", () => {
       ctx.app.use((event) => getRequestFingerprint(event, { hash: false }));
 
       ctx.app.config.onRequest = (event) => {
-        const { socket } = getNodeContext(event)?.req || {};
-        Object.defineProperty(socket, "remoteAddress", {
+        Object.defineProperty(event.node?.req.socket || {}, "remoteAddress", {
           get(): any {
             return "0.0.0.0";
           },
@@ -396,7 +393,7 @@ describe("", () => {
   describe("readFormDataBody", () => {
     it("can handle form as FormData in event handler", async () => {
       ctx.app.use("/api/*", async (event) => {
-        const formData = await readFormDataBody(event);
+        const formData = await event.request.formData();
         const user = formData!.get("user");
         expect(formData instanceof FormData).toBe(true);
         expect(user).toBe("john");
diff --git a/test/validate.test.ts b/test/validate.test.ts
index 3a6e852..aa020b5 100644
--- a/test/validate.test.ts
+++ b/test/validate.test.ts
@@ -1,7 +1,7 @@
 import type { ValidateFunction } from "../src/types";
 import { describe, it, expect, beforeEach } from "vitest";
 import { z } from "zod";
-import { readValidatedJSONBody, getValidatedQuery } from "../src";
+import { readValidatedBody, getValidatedQuery } from "../src";
 import { setupTest } from "./_setup";
 
 // Custom validator
@@ -27,15 +27,15 @@ const zodValidate = z.object({
 describe("Validate", () => {
   const ctx = setupTest();
 
-  describe("readValidatedJSONBody", () => {
+  describe("readValidatedBody", () => {
     beforeEach(() => {
       ctx.app.use("/custom", async (event) => {
-        const data = await readValidatedJSONBody(event, customValidate);
+        const data = await readValidatedBody(event, customValidate);
         return data;
       });
 
       ctx.app.use("/zod", async (event) => {
-        const data = await readValidatedJSONBody(event, zodValidate);
+        const data = await readValidatedBody(event, zodValidate);
         return data;
       });
     });
diff --git a/test/web.test.ts b/test/web.test.ts
index ce5b20f..c295866 100644
--- a/test/web.test.ts
+++ b/test/web.test.ts
@@ -1,10 +1,5 @@
 import { describe, it, expect } from "vitest";
-import {
-  readTextBody,
-  setResponseStatus,
-  getRequestHeaders,
-  getQuery,
-} from "../src";
+import { getQuery } from "../src";
 import { setupTest } from "./_setup";
 
 describe("Web handler", () => {
@@ -12,12 +7,13 @@ describe("Web handler", () => {
 
   it("works", async () => {
     ctx.app.use("/test/**", async (event) => {
-      const body = await readTextBody(event);
-      setResponseStatus(event, 201, "Created");
+      const body = await event.request.text();
+      event.response.status = 201;
+      event.response.statusText = "Created";
       return {
         method: event.method,
         path: event.path,
-        headers: getRequestHeaders(event),
+        headers: Object.fromEntries(event.request.headers.entries()),
         query: getQuery(event),
         body,
         contextKeys: Object.keys(event.context),