H3 (pronounced /eɪtʃθriː/) is a minimal HTTP framework built for high performance and portability. Currently on v2 — a major rewrite based on web standard primitives (Request, Response, URL, Headers).
# Setup
corepack enable && pnpm install
# Development
pnpm dev # vitest watch mode
pnpm vitest run <path> # run specific test
pnpm test # full suite (lint + typecheck + coverage)
pnpm build # build with obuild
pnpm lint # oxlint + oxfmt --check (lint + typecheck)
pnpm fmt # automd + oxlint --fix + oxfmt
pnpm bench:node # node benchmarks
pnpm bench:bun # bun benchmarks- Web standards first: Built on native
Request,Response,URL,Headers - Multi-runtime: Node.js, Bun, Deno, Cloudflare Workers, Service Workers, browsers
- Minimal core: 2 production deps (
rou3for routing,srvxfor server abstraction) - Handler-based: Composable handlers + middleware, no class-heavy patterns
- Type-safe: Strict TypeScript with generic inference throughout
| Class | File | Purpose |
|---|---|---|
H3 |
src/h3.ts |
Main app class (extends H3Core), adds routing methods (get/post/put/delete/...) |
H3Event |
src/event.ts |
Request wrapper — wraps web Request with lazy properties (URL, context) |
HTTPError |
src/error.ts |
Structured HTTP error with status, data, headers |
HTTPResponse |
src/response.ts |
Flexible response builder |
- Request enters via platform adapter (
src/_entries/*.ts) H3.fetch()createsH3EventfromRequest- Global
onRequesthooks run - Middleware chain executes (matched by route/method)
- Route handler processes request, returns a value
toResponse()converts return value →Response(auto-handles JSON, streams, blobs, primitives)- Global
onResponsehooks run
src/
├── index.ts # Public API exports
├── h3.ts # H3Core + H3 classes
├── event.ts # H3Event
├── handler.ts # defineHandler, defineValidatedHandler, etc.
├── middleware.ts # Middleware system
├── response.ts # toResponse, HTTPResponse, kNotFound, kHandled
├── error.ts # HTTPError
├── adapters.ts # Web/Node handler adapters
├── tracing.ts # Tracing plugin (separate entry point)
├── types/ # Type definitions
│ ├── h3.ts # App types (H3Config, H3Plugin, H3Route, HTTPMethod)
│ ├── handler.ts # Handler types (EventHandler, Middleware)
│ ├── context.ts # H3EventContext
│ └── _utils.ts # Internal type helpers
├── utils/ # ~30 utility modules (public API)
│ ├── request.ts # getQuery, getRouterParams, getRequestURL, ...
│ ├── response.ts # redirect, noContent, html, iterable, ...
│ ├── body.ts # readBody, readValidatedBody, assertBodySize
│ ├── cookie.ts # getCookie, setCookie, parseCookies, chunked cookies
│ ├── session.ts # getSession, useSession, sealSession, ...
│ ├── auth.ts # requireBasicAuth, basicAuth
│ ├── cors.ts # handleCors, appendCorsHeaders, ...
│ ├── proxy.ts # proxy, proxyRequest, fetchWithEvent
│ ├── ws.ts # defineWebSocketHandler, defineWebSocket
│ ├── json-rpc.ts # defineJsonRpcHandler, defineJsonRpcWebSocketHandler
│ ├── event-stream.ts # createEventStream (SSE)
│ ├── static.ts # serveStatic
│ ├── cache.ts # handleCacheHeaders
│ ├── middleware.ts # onRequest, onResponse, onError, bodyLimit
│ ├── route.ts # defineRoute
│ ├── base.ts # withBase
│ └── internal/ # Internal helpers (not exported)
│ ├── auth.ts, body.ts, cors.ts, encoding.ts, ...
│ ├── iron-crypto.ts # Session sealing crypto
│ ├── standard-schema.ts # Standard schema validation
│ └── validate.ts
├── _entries/ # Platform-specific entry points
│ ├── generic.ts # Web Worker / Browser
│ ├── node.ts # Node.js (adds serve())
│ ├── bun.ts # Bun
│ ├── deno.ts # Deno
│ ├── cloudflare.ts # Cloudflare Workers
│ ├── service-worker.ts # Service Workers
│ └── _common.ts # Shared entry utilities
└── _deprecated.ts # Deprecated exports (v1 compat)
test/
├── _setup.ts # Test infrastructure (describeMatrix, setupWebTest, setupNodeTest)
├── *.test.ts # ~30 integration test files
├── unit/ # Unit tests (including type tests: types.test-d.ts)
├── bench/ # Benchmarks (mitata)
└── fixture/ # Runtime-specific playground fixtures
- ESM only — no CommonJS
- Explicit
.tsextensions in all import paths - No barrel files — import directly from specific modules
- Internal files use
_prefix (e.g.,_deprecated.ts,_entries/,_utils.ts) - Internal helpers go at the end of files or in
utils/internal/ - Short files — aim for < 200 LoC per file, split when larger
- Options object as second param for multi-arg functions
- Formatting:
oxfmt(no config, uses defaults) - Linting:
oxlintwithunicorn,typescript,oxcplugins
kprefix for symbol constants (kNotFound,kHandled)~prefix for private/non-enumerable properties#for truly private class fieldsdefine*()for factory functions (defineHandler,defineMiddleware,defineWebSocketHandler)to*()for conversion functions (toResponse,toEventHandler,toWebHandler)from*()for adapter functions (fromWebHandler,fromNodeHandler)
- Strict mode +
isolatedDeclarations+verbatimModuleSyntax erasableSyntaxOnly: true(no enums, no namespaces)- Target/module:
ESNext/NodeNext - Lib:
["ESNext", "WebWorker", "DOM", "DOM.Iterable"] - Heavy use of generics for type inference in handlers
Handlers return values directly — no res.send() pattern:
- Return
string→ text response - Return
object→ JSON response - Return
Response/HTTPResponse→ direct response - Return
ReadableStream/Blob/File→ streamed response - Return
kNotFoundsymbol → 404 - Return
kHandledsymbol → already handled (SSE, WebSocket, etc.)
- Vitest v4+ with v8 coverage
- Matrix testing: every test runs in both
webandnodemodes
import { describeMatrix } from "./_setup.ts";
describeMatrix("feature name", (ctx, { it, expect }) => {
it("does something", async () => {
ctx.app.get("/test", () => "hello");
const res = await ctx.fetch("/test");
expect(await res.text()).toBe("hello");
});
});Key patterns:
- Use
describeMatrixfor cross-runtime tests ctx.appis a freshH3instance per test (viabeforeEach)ctx.fetchhandles URL resolution for both web/nodectx.errorstracks unhandled errors (auto-asserted inafterEach)- Use
it.skipIf(ctx.target === "node")for runtime-specific skips
pnpm vitest run test/body.test.ts # single file
pnpm vitest run test/unit/ # unit tests
pnpm dev # watch mode (all)
pnpm test # full: lint + typecheck + coverage- Write regression test that reproduces the bug
- Confirm test fails before any code changes
- Fix the implementation (minimal change)
- Confirm test passes
- Run broader test suite for regressions
- obuild with Rolldown bundler
- 6 platform entries +
tracing.tsas separate entry - Code splitting enabled (
h3-[hash].mjschunks) - Custom plugin strips comments (preserves
#/@annotations) - Output:
dist/_entries/*.mjs+dist/*.d.mts
h3 → auto-resolved by runtime (deno/bun/workerd/node/default)
h3/node → Node.js with serve()
h3/bun → Bun runtime
h3/deno → Deno runtime
h3/cloudflare → Cloudflare Workers
h3/service-worker → Service Workers
h3/generic → Universal web standard
h3/tracing → Tracing plugin
| Dep | Purpose |
|---|---|
rou3 |
Route matching engine |
srvx |
Server abstraction (multi-runtime) |
crossws |
WebSocket abstraction (optional peer dep) |
- Prefer web standard APIs over runtime-specific ones
- Keep the core minimal — add utilities, not core complexity
- Test across runtimes using
describeMatrix - Return values from handlers instead of mutating responses
- Use
defineHandler/defineMiddlewarefor type safety